Chào mừng đến với bài hướng dẫn số 14 trong series Hướng dẫn lập trình Golang cơ bản. Trong bài này chúng ta sẽ nói về kiểu dữ liệu string ("chuỗi").
String là một kiểu dữ liệu thường gặp nhất trong ngôn ngữ lập trình. Do có một số khác biệt trong việc sử dụng so với các ngôn ngữ khác, chúng tôi dành riêng phần này để nói về string.
String là gì?
String trong Go là một slice chứa các byte. String có thể được tạo ra bằng cách bao quanh nội dung của nó trong dấu “ “. Hãy xem một ví dụ đơn giản dưới đây, tạo một string và in ra màn hình:
package main
import (
"fmt"
)
func main() {
name := "Hello World"
fmt.Println(name)
}
Chương trình trên khi chạy sẽ in ra dòng chữ Hello World.
String trong Go tuân theo chuẩn Unicode và mã hóa UTF-8.
Truy cập các byte trong một string
Do string là một slice của byte, chúng ta có thể truy cập từng phần tử byte của một string.
package main
import (
"fmt"
)
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func main() {
name := "Hello World"
printBytes(name)
}
Tại dòng thứ 8 ví dụ trên, hàm len(s) trả về số byte của string, chúng ta sử dụng vòng lặp for để in ra từng byte này dưới dạng hexadecimal (thập lục phân). Kiểu định dạng %x để in ra ký hiệu hexadecimal. Chương trình trên sẽ in ra chuỗi 48 65 6c 6c 6f 20 57 6f 72 6c 64. Chuỗi này chính là giá trị đã mã hóa dưới dạng Unicode UTF8 của từ “Hello World”. Để hiểu rõ hơn về string, các bạn cần hiểu một cách cơ bản về Unicode và UTF-8. Các bạn có thể đọc thêm về Unicode và UTF tại naveenr.net.
Chúng ta sẽ sửa ví dụ trên một chút để chương trình in ra các ký tự của string:
package main
import (
"fmt"
)
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%c ",s[i])
}
}
func main() {
name := "Hello World"
printBytes(name)
fmt.Printf("\n")
printChars(name)
}
Tại dòng 16 của method printChars, kiểu định dạng %c được dùng để in ra các ký tự trong string:
48 65 6c 6c 6f 20 57 6f 72 6c 64
H e l l o W o r l d
Mặc dù chương trình trên trông có vẻ hợp lệ để truy cập từng ký tự trong một string, nó vẫn có một lỗi (bug) nghiêm trọng. Hãy thử phân tích code để tìm lỗi này:
package main
import (
"fmt"
)
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%c ",s[i])
}
}
func main() {
name := "Hello World"
printBytes(name)
fmt.Printf("\n")
printChars(name)
fmt.Printf("\n")
name = "Señor"
printBytes(name)
fmt.Printf("\n")
printChars(name)
}
Chương trình trên sẽ in ra:
48 65 6c 6c 6f 20 57 6f 72 6c 64
H e l l o W o r l d
53 65 c3 b1 6f 72
S e à ± o r
Tại dòng 28 của ví dụ trên, chúng ta muốn in ra từng ký tự của từ Señor, tuy nhiên chương trình lại in ra S e à ± o r là không đúng. Tại sao với từ Señor chương trình in ra bị lỗi, trong khi với từ Hello World vẫn in đúng? Nguyên nhân là mã Unicode của ký tự ñ là U+00F1, và mã hóa UTF-8 giữ 2 byte c3 và b1. Chúng ta muốn in ra các ký tự với giả thuyết rằng mỗi điểm mã (code point) sẽ có độ dài 1 byte là sai. Trong chuẩn mã hóa UTF-8, mỗi điểm mã có thể chứa nhiều hơn 1 byte. Vậy chúng ta giải quyết vấn đề này thế nào? Đây là lúc cần đến “rune”.
Rune
Một rune là một kiểu dữ liệu có sẵn trong Go, một cách gọi khác của kiểu dữ liệu int32. Rune đại diện cho một điểm mã trong Go. Không quan trọng điểm mã chứa bao nhiêu byte, nó có thể đại diện bởi một rune. Chúng ta hãy sửa ví dụ trên để in ra ký tự sử dụng rune:
package main
import (
"fmt"
)
func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}
func printChars(s string) {
runes := []rune(s)
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}
func main() {
name := "Hello World"
printBytes(name)
fmt.Printf("\n")
printChars(name)
fmt.Printf("\n\n")
name = "Señor"
printBytes(name)
fmt.Printf("\n")
printChars(name)
}
Tại dòng 14, string được chuyển đổi thành một slice chứa các rune. Chúng ta sử dụng vòng lặp để in ra các ký tự, chương trình sẽ in ra:
48 65 6c 6c 6f 20 57 6f 72 6c 64
H e l l o W o r l d
53 65 c3 b1 6f 72
S e ñ o r
Perfect! Kết quả trên đúng như mong muốn của chúng ta.
Vòng lặp for range của một string
Ví dụ phần trên là một cách hoàn hảo để lặp từng phần tử rune của một string. Tuy nhiên, Go cung cấp một cách dễ dàng hơn để làm được điều đó, bằng cách sử dụng vòng lặp for ... range.
package main
import (
"fmt"
)
func printCharsAndBytes(s string) {
for index, rune := range s {
fmt.Printf("%c starts at byte %d\n", rune, index)
}
}
func main() {
name := "Señor"
printCharsAndBytes(name)
}
Tại dòng 8, string được lặp sử dụng for range. Vòng lặp trả về vị trí của từng byte tương ứng với rune. Chương trình sẽ in ra:
S starts at byte 0
e starts at byte 1
ñ starts at byte 2
o starts at byte 4
r starts at byte 5
Nhìn vào kết quả trên, rõ ràng ký tự ñ chứa 2 byte :).
Thiết lập string từ slice chứa các byte
package main
import (
"fmt"
)
func main() {
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)
fmt.Println(str)
}
Biến byteSlice trong ví dụ trên chứa các mã UTF-8 dạng hex của từ "Café". Kết quả chương trình trên sẽ in ra Café.
Điều gì sẽ xảy ra nếu chúng ta thay mã dạng hex bằng dạng decimal tương ứng? Chương trình có làm việc hay không? Hãy thử xem:
package main
import (
"fmt"
)
func main() {
byteSlice := []byte{67, 97, 102, 195, 169} // tương đương phần thập phân của {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
str := string(byteSlice)
fmt.Println(str)
}
Chương trình vẫn in ra từ Café.
Tạo string từ slice chứa các rune
package main
import (
"fmt"
)
func main() {
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
str := string(runeSlice)
fmt.Println(str)
}
Trong ví dụ trên, biến runeSlice chứa các điểm mã Unicode của từ Señor dạng hexadecimal. Chương trình sẽ in ra Señor.
Độ dài của string
Chúng ta cùng xem ví dụ:
package main
import (
"fmt"
"unicode/utf8"
)
func length(s string) {
fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s))
}
func main() {
word1 := "Señor"
length(word1)
word2 := "Pets"
length(word2)
}
Hàm RuneCountInString(s string) (n int) của gói UTF8 thường được sử dụng để tìm độ dài của string. Phương thức này coi string như một đối số và trả về số lượng rune trong đó.
Kết quả in ra:
length of Señor is 5
length of Pets is 4
Kiểu dữ liệu string là bất biến
Kiểu dữ liệu string là bất biến, có nghĩa là, một khi string được tạo ra, nó không thể bị thay đổi. Hãy cùng xem ví dụ dưới đây:
package main
import (
"fmt"
)
func mutate(s string)string {
s[0] = 'a' // bất kỳ ký tự unicode hợp lệ trong ngoặc nháy đơn đều là một rune
return s
}
func main() {
h := "hello"
fmt.Println(mutate(h))
}
Tại dòng thứ 8, chúng ta muốn thay đổi ký tự đầu tiên của string thành ‘a’. Ngôn ngữ Go không cho phép làm vậy do string là bất biến, do đó chương trình sẽ trả về lỗi main.go:8: cannot assign to s[0].
Để làm việc được với string, chúng ta chuyển string thành slice chứa các rune, chúng ta thay đổi slice và sau đó chuyển lại thành string mới.
package main
import (
"fmt"
)
func mutate(s []rune) string {
s[0] = 'a'
return string(s)
}
func main() {
h := "hello"
fmt.Println(mutate([]rune(h)))
}
Tại dòng thứ 7, hàm mutate chấp nhận một rune slice như đối số, sau đó thay đổi phần tử đầu tiên của slice thành a, chuyển đổi rune lại thành string và trả về nó. Method này được gọi từ dòng 13 của chương trình. h được chuyển đổi từ một phần tử rune trong slice và đi qua hàm mutate. Chương trình sẽ trả về aello.
Phần hướng dẫn về string đến đây là kết thúc. Xin vui lòng để lại những ý kiến và phản hồi của các bạn.
Hẹn gặp lại trong bài học tiếp theo: Con trỏ - Pointers.
Bình luận