Chuỗi trong Go

Chuỗi (string) xứng đáng được đề cập đặc biệt trong Go vì chúng khác biệt trong cách triển khai khi so sánh với các ngôn ngữ khác.

Chuỗi là gì?

Chuỗi là một slice của byte trong Go. String có thể được tạo bằng cách đặt một tập hợp các ký tự bên trong cặp dấu ngoặc kép "".

Hãy xem một ví dụ đơn giản tạo một string và in nó.

package main

import (  
    "fmt"
)

func main() {  
    name := "Hello World"
    fmt.Println(name)
}

Chạy chương trình trong playground

Chương trình trên sẽ in ra:

Hello World

String trong Go tuân thủ Unicode và được mã hóa UTF-8.

Truy cập từng byte riêng lẻ của một chuỗi

Vì một string là một slice của byte, nên có thể truy cập từng byte của một string.

package main

import (  
    "fmt"
)

func printBytes(s string) {  
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func main() {  
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printBytes(name)
}

Chạy chương trình trong playground

%s là mã định dạng để in một chuỗi. Trong dòng số 16, chuỗi đầu vào được in. Trong dòng số 9 của chương trình trên, len(s) trả về số byte trong chuỗi và chúng ta sử dụng vòng lặp for để in các byte đó dưới dạng ký hiệu thập lục phân. %x là mã định dạng cho hệ thập lục phân. Kết quả chương trình trên:

String: Hello World  
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64  

Đây là các giá trị được mã hóa Unicode UT8 của chuỗi Hello World. Cần có hiểu biết cơ bản về Unicode và UTF-8 để hiểu các chuỗi tốt hơn. Tôi khuyên bạn nên đọc bài viết dưới đây để biết thêm về Unicode và UTF-8.

Unicode Character Set and UTF-8, UTF-16, UTF-32 Encoding - naveenr
Unicode character set maps every character in the world to a unique number. UTF-8, UTF-16 and UTF-32 are encoding schemes to represent the unicode code points in memory.

Truy cập các ký tự riêng lẻ của một chuỗi

Hãy sửa đổi chương trình trên một chút để in các ký tự của chuỗi.

package main

import (  
    "fmt"
)

func printBytes(s string) {  
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {  
    fmt.Printf("Characters: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i])
    }
}

func main() {  
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    fmt.Printf("\n")
    printBytes(name)
}

Chạy chương trình trong playground

Trong dòng 17 của chương trình trên, mã định dạng %c được sử dụng để in các ký tự của chuỗi trong hàm printChars. Chương trình in ra kết quả sau:

String: Hello World  
Characters: H e l l o   W o r l d  
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64  

Mặc dù chương trình trên trông giống như một cách hợp pháp để truy cập các ký tự riêng lẻ của một chuỗi, nhưng điều này có một lỗi nghiêm trọng. Hãy cùng tìm hiểu lỗi đó là gì.

package main

import (  
    "fmt"
)

func printBytes(s string) {  
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {  
    fmt.Printf("Characters: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c ", s[i])
    }
}

func main() {  
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    fmt.Printf("\n")
    printBytes(name)
    fmt.Printf("\n\n")
    name = "Señor"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    fmt.Printf("\n")
    printBytes(name)
}

Chạy chương trình trong playground

Kết quả của chương trình trên là:

String: Hello World  
Characters: H e l l o   W o r l d  
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64 

String: Señor  
Characters: S e à ± o r  
Bytes: 53 65 c3 b1 6f 72  

Trong dòng 30 của chương trình trên, chúng ta đang cố gắng in các ký tự của Señor và nó in ra S e à ± o r là không chính xác. Tại sao chương trình này bị hỏng với Señor trong khi nó hoạt động hoàn toàn tốt cho Hello World.

Lý do là mã Unicode của ñU+00F1mã hóa UTF-8 của nó chiếm 2 byte c3b1. Chúng ta đang cố gắng in các ký tự giả sử rằng mỗi mã sẽ dài một byte, điều này là sai.

Trong mã hóa UTF-8, một mã có thể chiếm nhiều hơn 1 byte. Vậy chúng ta giải quyết điều này như thế nào? Đây là nơi mà rune cứu chúng ta.

Rune trong Go

Rune là một kiểu dữ liệu tích hợp sẵn trong Go và nó là bí danh của int32. Rune đại diện cho một mã Unicode trong Go. Không quan trọng mã chiếm bao nhiêu byte, nó có thể được biểu thị bằng rune. Hãy sửa đổi chương trình trên để in các ký tự bằng rune.

package main

import (  
    "fmt"
)

func printBytes(s string) {  
    fmt.Printf("Bytes: ")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
}

func printChars(s string) {  
    fmt.Printf("Characters: ")
    runes := []rune(s)
    for i := 0; i < len(runes); i++ {
        fmt.Printf("%c ", runes[i])
    }
}

func main() {  
    name := "Hello World"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    fmt.Printf("\n")
    printBytes(name)
    fmt.Printf("\n\n")
    name = "Señor"
    fmt.Printf("String: %s\n", name)
    printChars(name)
    fmt.Printf("\n")
    printBytes(name)
}

Chạy chương trình trong playground

Trong dòng 16 của chương trình trên, chuỗi được chuyển đổi thành một slice kiểu rune. Sau đó, chúng ta duyệt qua nó và hiển thị các ký tự. Chương trình này in ra:

String: Hello World  
Characters: H e l l o   W o r l d  
Bytes: 48 65 6c 6c 6f 20 57 6f 72 6c 64 

String: Señor  
Characters: S e ñ o r  
Bytes: 53 65 c3 b1 6f 72  

Thật hoàn hảo😀.

Truy cập các rune riêng lẻ bằng vòng lặp for range

Chương trình trên là một cách hoàn hảo để duyệt qua các rune riêng lẻ của một chuỗi. Nhưng Go cung cấp cho chúng ta một cách dễ dàng hơn nhiều để thực hiện việc này bằng cách sử dụng vòng lặp for range.

package main

import (  
    "fmt"
)

func charsAndBytePosition(s string) {  
    for index, rune := range s {
        fmt.Printf("%c starts at byte %d\n", rune, index)
    }
}

func main() {  
    name := "Señor"
    charsAndBytePosition(name)
}

Chạy chương trình trong playground

Trong dòng 8 của chương trình trên, chuỗi được duyệt qua bằng cách sử dụng vòng lặp for range. Vòng lặp trả về vị trí của byte mà rune bắt đầu cùng với rune. Chương trình này xuất 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  

Từ đầu ra ở trên, rõ ràng là ñ chiếm 2 byte vì ký tự tiếp theo o bắt đầu ở byte 4 thay vì byte 3 😀.

Tạo một chuỗi từ một slice của byte

package main

import (  
    "fmt"
)

func main() {  
    byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
    str := string(byteSlice)
    fmt.Println(str)
}

Chạy chương trình trong playground

Slice byteSlice ở dòng số 8 của chương trình trên chứa các byte hex được mã hóa UTF-8 của chuỗi Café. Chương trình in ra kết quả sau:

Café  

Điều gì sẽ xảy ra nếu chúng ta có giá trị thập phân tương đương với các giá trị hex. Liệu chương trình trên có hoạt động không? Hãy cùng kiểm tra nào.

package main

import (  
    "fmt"
)

func main() {  
    byteSlice := []byte{67, 97, 102, 195, 169}//decimal equivalent of {'\x43', '\x61', '\x66', '\xC3', '\xA9'}
    str := string(byteSlice)
    fmt.Println(str)
}

Chạy chương trình trong playground

Các giá trị thập phân cũng hoạt động và chương trình trên cũng sẽ in ra:

Café

Tạo một chuỗi từ một slice của rune

package main

import (  
    "fmt"
)

func main() {  
    runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
    str := string(runeSlice)
    fmt.Println(str)
}

Chạy chương trình trong playground

Trong chương trình trên slice runeSlice có chứa các mã Unicode của chuỗi Señor trong hệ thập lục phân. Kết quả chương trình

Señor  

Độ dài của chuỗi

Hàm RuneCountInString(s string) (n int) của gói utf8 có thể được sử dụng để tìm độ dài của chuỗi. Hàm này nhận một chuỗi làm đối số và trả về số lượng rune trong đó.

Như chúng ta đã thảo luận trước đó, len(s) được sử dụng để tìm số byte trong chuỗi và nó không trả về độ dài chuỗi. Như chúng ta đã thảo luận, một số ký tự Unicode có các điểm mã chiếm hơn 1 byte. Sử dụng len để tìm ra độ dài của các chuỗi đó sẽ trả về độ dài chuỗi không chính xác.

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    word1 := "Señor"
    fmt.Printf("String: %s\n", word1)
    fmt.Printf("Length: %d\n", utf8.RuneCountInString(word1))
    fmt.Printf("Number of bytes: %d\n", len(word1))

    fmt.Printf("\n")
    word2 := "Pets"
    fmt.Printf("String: %s\n", word2)
    fmt.Printf("Length: %d\n", utf8.RuneCountInString(word2))
    fmt.Printf("Number of bytes: %d\n", len(word2))
}

Chạy chương trình trong playground

Kết quả của chương trình trên là:

String: Señor  
Length: 5  
Number of bytes: 6

String: Pets  
Length: 4  
Number of bytes: 4  

Kết quả ở trên xác nhận hàm len(s)RuneCountInString(s) trả về các giá trị khác nhau 😀.

So sánh hai chuỗi

Toán tử == được sử dụng để so sánh bằng hai chuỗi. Nếu cả hai chuỗi bằng nhau, thì kết quả là true, ngược lại là false.

package main

import (  
    "fmt"
)

func compareStrings(str1 string, str2 string) {  
    if str1 == str2 {
        fmt.Printf("%s and %s are equal\n", str1, str2)
        return
    }
    fmt.Printf("%s and %s are not equal\n", str1, str2)
}

func main() {  
    string1 := "Go"
    string2 := "Go"
    compareStrings(string1, string2)

    string3 := "hello"
    string4 := "world"
    compareStrings(string3, string4)

}

Chạy chương trình trong playground

Trong hàm compareStrings trên, dòng số 8 so sánh xem hai chuỗi str1str2 có bằng nhau hay không bằng cách sử dụng toán tử ==. Nếu chúng bằng nhau, nó sẽ in ra một thông báo tương ứng và thoát khỏi hàm.

Chương trình trên in ra:

Go and Go are equal  
hello and world are not equal  

Nối chuỗi

Có nhiều cách để thực hiện nối chuỗi trong Go. Hãy tham khảo một vài cách trong số chúng.

Cách đơn giản nhất để thực hiện nối chuỗi là sử dụng toán tử +.

package main

import (  
    "fmt"
)

func main() {  
    string1 := "Go"
    string2 := "is awesome"
    result := string1 + " " + string2
    fmt.Println(result)
}

Chạy chương trình trong playground

Trong chương trình trên, ở dòng 10, string1 được nối string2 với một khoảng trắng ở giữa. Chương trình này in ra:

Go is awesome  

Cách thứ hai để nối các chuỗi là sử dụng hàm Sprintf của gói fmt.

Hàm Sprintf nối các chuỗi theo định dạng đầu vào và trả về chuỗi kết quả. Hãy viết lại chương trình trên bằng hàm Sprintf.

package main

import (  
    "fmt"
)

func main() {  
    string1 := "Go"
    string2 := "is awesome"
    result := fmt.Sprintf("%s %s", string1, string2)
    fmt.Println(result)
}

Chạy chương trình trong playground

Trong dòng số 10 của chương trình trên, %s %s là đầu vào định dạng cho Sprintf. Định dạng này lấy hai chuỗi làm đầu vào và có một khoảng trắng ở giữa. Điều này sẽ nối hai chuỗi với một khoảng trắng ở giữa. Chuỗi kết quả được lưu trữ trong result. Chương trình này cũng in ra kết quả:

Go is awesome  

Chuỗi là bất biến

Các chuỗi là bất biến trong Go. Sau khi một chuỗi được tạo, bạn không thể thay đổi nó.

package main

import (  
    "fmt"
)

func mutate(s string)string {  
    s[0] = 'a'//any valid unicode character within single quote is a rune 
    return s
}
func main() {  
    h := "hello"
    fmt.Println(mutate(h))
}

Chạy chương trình trong playground

Trong dòng số 8 của chương trình trên, chúng ta cố gắng thay đổi ký tự đầu tiên của chuỗi thành 'a'. Bất kỳ ký tự Unicode hợp lệ nào trong một dấu ngoặc kép đều là rune.

Chúng ta cố gắng gán rune a vào vị trí thứ 0 của slice. Điều này không được phép vì chuỗi là bất biến và do đó chương trình không biên dịch được với lỗi:

./prog.go:8:7: cannot assign to s[0]

Để làm việc với chuỗi bất biến, chuỗi cần được chuyển đổi thành một slice của rune. Sau đó, có thể thay đổi các phần tử trong slice và được chuyển đổi trở lại thành một chuỗi 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)))
}

Chạy chương trình trong playground

Trong dòng số 7 của chương trình trên, hàm mutate chấp nhận một slice của rune làm đối số. Sau đó, nó thay đổi phần tử đầu tiên của slice thành 'a', chuyển đổi rune trở lại thành chuỗi và trả về nó.

Hàm này được gọi từ dòng số 13 của chương trình. Chuỗi h được chuyển đổi thành một slice của rune và được truyền đến hàm mutate tại dòng số 13. Chương trình này xuất kết quả sau:

aello

Như thường lệ, cảm ơn bạn đã đọc. Hãy chia sẻ những nhận xét và phản hồi có giá trị của bạn.

Go
Bài Viết Liên Quan:
Phương thức (method) trong Go
Trung Nguyen 02/12/2021
Phương thức (method) trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu phương thức (method) trong Go là gì? Cú pháp khai báo phương thức, so sánh phương thức với hàm, ... trong Go.

Struct trong Go
Trung Nguyen 28/11/2021
Struct trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu struct là gì, cách khai báo và sử dụng một struct trong Go, struct ẩn danh, so sanh hai struct, ...

Con trỏ trong Go
Trung Nguyen 28/11/2021
Con trỏ trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách con trỏ (pointer) hoạt động trong Go và nó khác với con trỏ trong các ngôn ngữ khác như C và C++ như thế nào.

Map trong Go
Trung Nguyen 28/11/2021
Map trong Go

Trong hướng dẫn này, chúng ta sẽ tìm hiểu map là gì, cú pháp khai báo map, thêm phần tử, xóa phần tử, duyệt các phần tử, ... của map trong Go.