Skip to content

Chuỗi trong Go

Trong Go, về bản chất chuỗi là một dãy byte chỉ đọc không thể thay đổi (byte sequence), ở đây "dãy byte" chỉ là dữ liệu cơ sở của chuỗi được tạo thành từ một dãy byte được sắp xếp theo thứ tự, những byte này chiếm một vùng bộ nhớ liên tục.

Giá trị chữ

Như đã đề cập trước đó, chuỗi có hai cách biểu diễn giá trị chữ, chia thành chuỗi thông thường và chuỗi nguyên thủy.

Chuỗi thông thường

Chuỗi thông thường được biểu thị bằng dấu nháy kép "", hỗ trợ thoát, không hỗ trợ viết nhiều dòng, dưới đây là một số chuỗi thông thường

go
"Đây là một chuỗi thông thường\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
Đây là một chuỗi thông thường
abcdefghijlmn
opqrst  \uvwxyz

Chuỗi nguyên thủy

Chuỗi nguyên thủy được biểu thị bằng dấu nháy ngược, không hỗ trợ thoát, hỗ trợ viết nhiều dòng, tất cả các ký tự trong chuỗi nguyên thủy sẽ được xuất ra nguyên vẹn, bao gồm xuống dòng và thụt lề.

go
`Đây là một chuỗi nguyên thủy, xuống dòng
  thụt lề tab, \t ký tự tab nhưng không hiệu lực, xuống dòng
  "Đây là một chuỗi thông thường"

  Kết thúc
`
Đây là một chuỗi nguyên thủy, xuống dòng
        thụt lề tab, \t ký tự tab nhưng không hiệu lực, xuống dòng
        "Đây là một chuỗi thông thường"

        Kết thúc

Truy cập

Vì về bản chất chuỗi là dãy byte, thao tác chỉ số str[i] được thiết kế để trả về byte thứ i, về cú pháp giống với slice, ví dụ truy cập phần tử đầu tiên của chuỗi

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

Xuất ra là giá trị mã byte chứ không phải ký tự

116

Cắt chuỗi

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

Thử sửa đổi phần tử chuỗi

go
func main() {
   str := "this is a string"
   str[0] = 'a' // Không thể biên dịch
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

Tuy không thể sửa đổi chuỗi, nhưng có thể ghi đè

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

Chuyển đổi

Chuỗi có thể chuyển đổi thành slice byte, và slice byte hoặc dãy byte cũng có thể chuyển đổi thành chuỗi, ví dụ như sau:

go
func main() {
   str := "this is a string"
   // Chuyển đổi kiểu rõ ràng thành slice byte
   bytes := []byte(str)
   fmt.Println(bytes)
   // Chuyển đổi kiểu rõ ràng thành chuỗi
   fmt.Println(string(bytes))
}

Nội dung của chuỗi là chỉ đọc không thể thay đổi, không thể sửa đổi, nhưng slice byte thì có thể sửa đổi.

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // Sửa đổi slice byte
  bytes = append(bytes, 96, 97, 98, 99)
    // Gán cho chuỗi ban đầu
  str = string(bytes)
  fmt.Println(str)
}

Sau khi chuyển đổi chuỗi thành slice byte, giữa hai cái hoàn toàn không có liên quan, vì Go sẽ phân bổ một vùng bộ nhớ mới cho slice byte, sau đó sao chép bộ nhớ của chuỗi qua, việc sửa đổi slice byte sẽ không gây ảnh hưởng gì cho chuỗi ban đầu, làm như vậy là để an toàn bộ nhớ.

Trong trường hợp này, nếu chuỗi hoặc slice byte cần chuyển đổi rất lớn, thì chi phí hiệu năng sẽ rất cao. Nhưng bạn cũng có thể thông qua thư viện unsafe để thực hiện chuyển đổi không sao chép, nhưng vấn đề an toàn đằng sau cần tự mình gánh vác, ví dụ như ví dụ dưới đây, địa chỉ của b1 và s1 là giống nhau.

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

Độ dài

Độ dài của chuỗi, thực ra không phải là số lượng ký tự, mà là độ dài của dãy byte. Chỉ là trong hầu hết thời gian chúng ta xử lý là ký tự ASCII, mỗi ký tự vừa hay có thể biểu diễn bằng một byte, nên độ dài byte và số lượng ký tự vừa hay bằng nhau. Để tính độ dài chuỗi sử dụng hàm tích hợp len, ví dụ như sau:

go
func main() {
   str := "this is a string" // Trông độ dài là 16
   str2 := "Đây là một chuỗi" // Trông độ dài là 7
   fmt.Println(len(str), len(str2))
}
16 21

Trông chuỗi tiếng Trung ngắn hơn chuỗi tiếng Anh, nhưng độ dài thực tế求 được lại dài hơn chuỗi tiếng Anh. Đây là vì trong mã hóa unicode, một ký tự Hán trong hầu hết trường hợp chiếm 3 byte, một ký tự tiếng Anh chỉ chiếm một byte, thông qua xuất phần tử đầu tiên của chuỗi có thể thấy kết quả:

go
func main() {
   str := "this is a string"
   str2 := "Đây là một chuỗi"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // Chữ cái t
è // "Mảnh vỡ" (byte đầu tiên) của một ký tự Trung văn, trùng hợp với giá trị mã hóa của ký tự Ý è
这 // Chữ Hán

Sao chép

Tương tự như cách sao chép của mảng slice, sao chép chuỗi thực ra là sao chép slice byte, sử dụng hàm tích hợp copy

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

Cũng có thể sử dụng hàm strings.clone, nhưng thực ra việc thực hiện bên trong đều gần như nhau

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

Nối

Việc nối chuỗi sử dụng toán tử +

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

Cũng có thể chuyển đổi thành slice byte rồi thêm phần tử

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

Hiệu năng của hai cách nối trên đều rất kém, trong trường hợp chung có thể sử dụng, nhưng nếu có yêu cầu cao hơn về hiệu năng, có thể sử dụng strings.Builder

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

Duyệt

Ở đầu bài viết này đã đề cập, chuỗi trong Go chính là một slice byte chỉ đọc, tức là đơn vị cấu thành của chuỗi là byte chứ không phải ký tự. Tình huống này thường gặp khi duyệt chuỗi, ví dụ đoạn mã dưới đây

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

Trong ví dụ lần lượt xuất dạng thập phân và thập lục phân của byte.

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

Do các ký tự trong ví dụ đều thuộc ký tự ASCII, chỉ cần một byte là có thể biểu diễn, nên kết quả vừa hay mỗi byte tương ứng với một ký tự. Nhưng nếu chứa ký tự không phải ASCII thì kết quả lại khác, như sau

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

Thông thường, một ký tự Trung văn sẽ chiếm 3 byte, nên có thể thấy kết quả sau

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

Duyệt theo byte sẽ tách ký tự Trung văn ra, điều này rõ ràng sẽ xuất hiện mã loạn. Chuỗi trong Go được xác định rõ hỗ trợ UTF-8, đối phó với tình huống này cần dùng đến kiểu rune, khi sử dụng for range để duyệt, đơn vị duyệt mặc định của nó là một rune, ví dụ đoạn mã dưới đây

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

Xuất ra như sau

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune về bản chất là bí danh kiểu của int32, phạm vi của bộ ký tự unicode nằm giữa 0x0000 - 0x10FFFF, tối đa chỉ có ba byte, số byte tối đa của mã hóa UTF-8 hợp lệ chỉ có 4 byte, nên việc sử dụng int32 để lưu trữ là đương nhiên, trong ví dụ trên chuyển đổi chuỗi thành []rune rồi duyệt cũng cùng một đạo lý, như sau

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

Cũng có thể sử dụng công cụ trong gói utf8, ví dụ

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

Kết quả xuất của hai ví dụ này đều giống nhau.

TIP

Về chi tiết hơn của chuỗi, có thể đến Strings, bytes, runes and characters in Go để tìm hiểu.

Golang by www.golangdev.cn edit