天天看点

Go中string与其它语言有什么不同?

作者:驯鹿的古牧

字符串在 Go 中值得特别提及,因为与其他语言相比,它们在实现上是不同的。

什么是string?

字符串string在Go中是一个bytes类型的切片。通过双引号创建字符串string。

name := "Hello World"           

Go 中的字符串符合 Unicode 标准,并且采用 UTF-8编码。

访问字符串的单个字节

由于字符串是bytes类型的切片,所以完全可以访问字符串的每个字节。

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

%s 是打印字符串的格式说明符。在第16行中,打印输入字符串。第9行中,len(s)返回字符串中的字节数,我们使用 for 循环以十六进制表示法打印这些字节。%x 是十六进制的格式说明符。以上程序输出,

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

访问字符串的单个字符

让我们简单修改上面的程序来打印字符串的字符。

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

上面程序中的第17行,printChars函数%c格式符用来打印字符串中的字符。会有如下打印:

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             

尽管上述程序看起来像是访问字符串各个字符的合法方法,但这有一个严重的bug。让我们找出该bug是什么。

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

上面的程序输出,

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            

在上面程序的第30行中,我们尝试打印Señor的字符,它输出S e à ± o r这是错误的。为什么这个程序在Señor中中断,而它对 Hello World 来说效果很好。原因是ñ的 Unicode 码位是 U+00F1,其 UTF-8编码占用2个字节c3 和b1。我们尝试打印字符,假设每个代码点将是一个字节长,这是错误的。在 UTF-8编码中,一个码位可以占用1个以上的字节。那么我们如何解决这个问题呢?这就是Rune拯救我们的地方。

Rune

rune是Go中的内置类型,它是int32的别名。Rune表示Go中的 Unicode 码位。无论代码点占用多少字节,它都可以用rune表示。让我们修改上面的程序以使用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)
}           

在上面程序的第16行中,字符串被转换为一rune切片。然后我们循环它并显示字符。该程序打印,

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            

使用for range循环访问rune

上面的程序是迭代字符串的各个rune的完美方法。但是 Go 为我们提供了一种更简单的方法来使用 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)
}           

在上面程序的第8行中,字符串使用 for range 循环进行迭代。循环返回rune与rune一起开始的字节的位置。此程序输出,

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

从上面的例子可以看出,ñ占两个字节,由于下一个字符o从byte 4开始而不是3。

从一个bytes切片创建字符串

package main

import (  
    "fmt"
)

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

上面程序第8行中的 byteSlice 包含字符串Café的UTF-8 编码十六进制字节。程序打印,

Café            

如果我们有十六进制值的十进制等效值会怎么样?上述程序会起作用吗?一起来看看吧。

package main

import (  
    "fmt"
)

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

十进制值也有效,上面的程序也将打印Café。

从rules切片创建字符串

package main

import (  
    "fmt"
)

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

在上面的程序中,runeSlice 包含十六进制字符串 Señor 的 Unicode 码位。程序输出,

Señor             

字符串的长度

utf8包的RuneCountInString(s string) (n int) 函数可用于查找字符串的长度。此方法将字符串作为参数,并返回其中的runes数。

len(s)用于查找字符串中的字节数,并且不返回字符串长度。正如我们已经讨论过的,某些 Unicode 字符的代码点占用超过1个字节。使用 len 找出这些字符串的长度将返回不正确的字符串长度。

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

上面的程序输出,

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

String: Pets  
Length: 4  
Number of bytes: 4             

字符串比较

==操作符常用于比较两个字符串是否相等。

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)

}           

在上面的 compareString 函数中,第8行使用 == 运算符比较两个字符串 str1 和 str2 是否相等。如果它们相等,则打印相应的消息,函数返回。上面的程序打印,

Go and Go are equal  
hello and world are not equal             

字符串拼接

Go中有多种方式进行字符串的拼接,让我们一一看一下。

最简单的方式用来拼接字符串是+运算符。

package main

import (  
    "fmt"
)

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

上面的程序中,string1和string2中间隔个空字符进行拼接,打印,

Go is awesome             

第二种方式是用fmt包中的Spintf函数。

Sprintf 函数根据输入格式说明符设置字符串的格式,并返回结果字符串。让我们使用 Sprintf 函数重写上面的程序。

package main

import (  
    "fmt"
)

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

在上面程序的第10行中,%s %s 是 Sprintf 的格式说明符输入。此格式说明符将两个字符串作为输入,并在两者之间有一个空格。这将连接两个字符串,中间有一个空格。生成的字符串存储在result中。该程序还打印,

Go is awesome             

字符串是不可变的

Go中字符串是不可变的,一旦字符串被被创建就不能修改它了。

package main

import (  
    "fmt"
)

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

在上述程序的第8行中,我们尝试将字符串的第一个字符更改为'a'。单引号内的任何有效 Unicode字符都是rune。我们尝试将rune a 分配给切片的第0个位置。这是不允许的,因为字符串是不可变的,因此程序无法编译并显示错误./prog.go:8:7:cannot assign s[0]

为了解决这种字符串不可变性的问题,字符串被转换为rune切片。然后,该切片会随着所需的任何更改而发生变异,并转换回新字符串。

package main

import (  
    "fmt"
)

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

在上述程序的第7行中,mutate函数接受rune切片作为参数。然后,它将切片的第一个元素更改为'a',将rune转换回字符串并返回它。此方法从程序的第13行调用。h被转换成一rune切片,并在第13行中传递到mutate。此程序输出aello。

总结

1.%s 是打印字符串的格式说明符;

%x是十六进制的格式说明符;

%c是字符格式说明符;由于UTF-8 编码中,一个代码点可以占用1个以上的字节,所以使用%c打印字符会出现bug,这时可以使用rune进行解决。

2. 使用for index, rune := range s 进行遍历rune字符;

3.len(s)用于查找字符串中的字节数;utf8包的RuneCountInString(s string) (n int) 函数可用于查找字符串的长度;

4.==操作符常用于比较两个字符串是否相等;

5.两种方式拼接字符串;

6.字符串是不可变的。