字符串

Golang中的字符串跟其他编程语言区别较大,因此这里专门抽出一节来介绍。

什么是字符串

Golang中的字符串数据类型是string。Golang中字符串本质上是一个由字节组成的切片。可以通过双引号来创建字符串。下面我们来创建一个字符串并打印。

package main

import "fmt"

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

Go Playground在线运行

该程序输出为:Hello World

Golang中字符串是通过UTF-8编码的(UTF-8是Unicode的实现方式之一)。

访问单个字节

前面说过,GOlang中字符串本质上是一个由字节组成的切片,因此我们当然可以访问组成字符串的单个字节。

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)
}

Go Playground在线运行 这里len(s)返回字符串的字节个数,然后我们通过for循环按照十六进制的格式打印出了每个字节。该程序输出为:48 65 6c 6c 6f 20 57 6f 72 6c 64。输出的这些字节其实就是Hello World这个字符串的UTF-8编码。通过这篇文章可以了解下什么是Unicode以及UTF-8编码。了解下Unicode和UTF-8的知识点非常有助于理解Golang中的字符串。

现在我们将前面这段代码修改如下:

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.Println()
    printChars(name)
}

Go Playground在线运行 这里的%c用来按字符打印字符串。该程序输出如下:

48 65 6c 6c 6f 20 57 6f 72 6c 64 
H e l l o   W o r l d

这里使用%c来打印字符串中的每个字符看起来是合法的,但其实是有问题的。我们换个字符串来打印试试看:

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 := "Señor"
    printBytes(name)
    fmt.Println()
    printChars(name)
}

Go Playground在线运行 该程序输出如下:

53 65 c3 b1 6f 72 
S e à ± o r

这里我们本来期望打印出Señor,却打印出了S e à ± o r。你肯定会疑惑,为什么打印Hello World能正常打印,换成Señor却不行。其实我们应该能看出来,这里是字符ñ打印成了à ±。为什么会这样呢?是因为字符ñ对应的Unicode码点为U+00F1,采用UTF-8编码会占用两个字节:c3b1,而前面程序中直接按照%c格式打印字符串是按照每个字符一个字节来打印的,因此才打印出了错误的结果。那怎么避免这种bug呢?Golang提供了rune(符文)数据类型来解决这个问题。

rune(符文)

Golang中rune也是一种基本数据类型,本质上是int32的别名,这一点在前面基本数据类型部分也提到过。Golang中rune用来表示一个Unicode码点,无论一个Unicode码点由几个字节组成(int32即为4个字节,4个字节来表示一个Unicode码点已经足够了),该码点均可由rune来表示。我们修改下前面的程序,使用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.Println()
    printChars(name)
    fmt.Println()
    name = "Señor"
    printBytes(name)
    fmt.Println()
    printChars(name)
}

Go Playground在线运行 这里runes := []rune(s)将字符串强制转换成了rune切片。然后我们基于rune来打印出了正常的字符。该程序执行结果如下,正是我们想要的。

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

使用for range循环访问字符串

前面几段程序中循环访问字符串每个字符的方法都挺好,但是Golang中还有一种更加简洁的方法: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)
}

Go Playground在线运行 这里我们使用for range来迭代访问字符串s。打印出了迭代访问到的每个字符以及其字节索引位置。该程序输出如下:

S starts at byte 0
e starts at byte 1
ñ starts at byte 2
o starts at byte 4
r starts at byte 5

从输出结果也能清晰的看出来,字符ñ占用了2个字节。

基于byte(字节)切片构建字符串

package main

import "fmt"

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

Go Playground在线运行 这里创建了一个切片byteSlice,该切片由十六进制形式的字节组成(一个字节包含8比特,这里的0x43就是一个字节)。这些字节本身是字符串CaféUTF-8的UTF-8编码。因此该程序输出为Café

当然,这里其实没必要非的用十六进制形式,比如下面换成十进制形式也可以:

package main

import "fmt"

func main() {
    byteSlice := []byte{67, 97, 102, 195, 169}
    str := string(byteSlice)
    fmt.Println(str)
}

Go Playground在线运行 输出结果仍然是Café

基于rune(符文)切片构建字符串

package main

import "fmt"

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

Go Playground在线运行 该程序中runeSlice由字符串Señor的Unicode编码码点组成,格式为十六进制形式。输出为:Señor

字符串长度

注意,之类说的字符串长度为字符串中字符(rune)的长度。 utf8包中的函数func RuneCountInString(s string) (n int)用来获取字符串长度。

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)
}

Go Playground在线运行 该程序执行结果如下:

length of Señor is 5
length of Pets is 4

immutable

immutable不可变类型,一旦创建完就不可修改。Golang中字符串就是不可变类型。

package main

import "fmt"

func mutate(s string) string {
    s[0] = 'a'
    return s
}

func main() {
    str := "hello"
    fmt.Println(mutate(str))
}

Go Playground在线运行 这里我们尝试将字符串的第一个字符改为a,由于字符串类型是immutable的,因此执行报错:

cannot assign to s[0]

如果我们确实想改变字符串中的某个字符,可以先将字符串转换为符文(rune)切片,通过切片去修改,完事之后再转换回字符串即可。

package main

import "fmt"

func mutate(s []rune) string {  
    s[0] = 'a' 
    return string(s)
}
func main() {  
    str := "hello"
    fmt.Println(mutate([]rune(str)))
}

Go Playground在线运行 该程序先将字符串str转换成了符文切片,通过切片将第一个字符改为a,然后再将切片转换回字符串。输出为:aello

Last updated