第十四篇 字符串string

欢迎来到Golang教程系列的第十四篇

相对于其它语言来说,Go的字符串的实现方式是不一样的,值得特别一提。

什么是字符串string?

在go中,字符串是字节的切片slice。string可以通过闭合双引号""圈起的字符来创建。

下面我们看一个简单的例子,创建一个string并且打印它。

package main

import (
	"fmt"
)

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

上面的程序将会打印Hello World

在Go中string是Unicode兼容且是用UTF-8编码的,参考https://www.cnblogs.com/swong/articles/15850566.html

访问字符串的单个字节

因为字符串是字节的切片,所以可以访问字符串的每一个字节。

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是打印字符串的格式符号,len(s)返回字符串的字节数,如果我们使用for loop打印这些字节的二进制符号,可以使用%x格式符号,上面程序输出:

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

这些是Hello WorldUnicode UTF8-encoded值,对于Unicode和UTF-8的基本理解是为了更好地了解字符串。我建议阅读https://www.cnblogs.com/swong/articles/15850566.html 去了解更多Unicode和UTF-8.

访问字符串的单个字符

我们稍微修改一下上面的程序,来打印字符串的字符。

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

上面程序中,在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: 73 65 c3 b1 6f 72 

在上面程序中,我们打印了señor的字符,但是给了错误的输出s e à ± o r,为什么这个程序打印Hello Wolrd的时候是正常的,但打印señor却是不正常了呢?原因就是ñ的Unicode code point是U+00F1,它的UTF-8 encoding占有两个字节c3b1。所以当我们打印的字符超过一个字节的就会打印错误了。在UTF-8编码中,一个代码点(code point)会占据超过一个字节,那我们如何解决这个问题呢?rune会帮到我们。

Rune

rune是Go的内置类型,它是int32的别名。Rune在Go中表示的是一个Unicode code point。它不用管一个code point占据多少个字节,都可以用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)

}

在上面程序中,runes := []rune(s),字符串string转化成了runes的切片,然后我们遍历它并展示字符,这个程序会打印。

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: 73 65 c3 b1 6f 72

上面的输出完美展示了我们需要的。

用for rang循环访问单个runes

上面的代码完美地迭代了字符串的单个runes。但是Go给我们提供了一个更简单的方法来做这事,就是使用for range循环

package main

import (
	"fmt"
)

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


func main() {

	name := "señor"
	charsAndBytePositon(name)

}

在上面程序中,使用了for range来迭代字符串,循环返回了字符串字节value开始的位置。下面是输出。

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是从4开始,而不是3.

根据字节的切片创建字符串

package main

import (
	"fmt"
)

func main() {

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

}

在上面程序中byteSlice包含CaféUTF-8 Encodeed十六进制字节。程序输出

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)

}

十进制的值也一样会运行并且输出同样的值。

根据runes的切片创建字符串

package main

import "fmt"

func main() {


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

}

上面的程序中,runeSlice包含了字符串Señor的用十六进制表示的Unicode code point。它的输出

Señor

字符串长度

utf8 package中有个RuneCountInString(s string) (n int)函数可以获取字符串长度,这个方法接收一个字符串参数,并返回runes数据数目。

正如我们早先讨论的,len(s)用来获取string的bytes数量的,它并不会返回字符串长度。我们早已讨论过,一些Unicode字符 code point占据超过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 os bytes: %d\n",len(word2))

}

上面的程序输出如下;

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

String: Pets
Length: 4
Number os bytes: 4

通过上面的输出,我们确认了len(s)RuneCountInString(s)返回的是不同的值。

字符串比较

==操作符是用来比较两个字符串是否相等,如果两个字符串相等,那么它就会返回true,否则就是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)
}

在上面的compareStrings函数中,if str1 == str2 使用了==操作符比较str1str2两个字符串是否相等,如果相等,那么就会打印相应的信息,并return。

上面程序输出:

Go and Go are equal
hello and world are not equal

字符串连结

在GO中有多种方式来实现连结字符串,我们看看其中的几个。

最简单实现字符串连结的方式是使用+操作符。


package main

import (
	"fmt"
)


func main() {

	string1 := "Go"
	string2 := "is awsome"
	result := string1 + " " + string2

	fmt.Println(result)
}

上面程序中,string1连结string2,而且中间带有一个空格。程序输出如下:

Go is awsome

第二种连结字符串的方法是使用Sprintd函数,它来自fmt package。

Sprintf函数格式化一个字符串,根据输入格式符号并且返回结果字符串。我们用Sprintf重写上面的程序。

package main

import (
	"fmt"
)


func main() {

	string1 := "Go"
	string2 := "is awsome"
	result := fmt.Sprintf("%s %s", string1, string2)

	fmt.Println(result)
}

在上面程序中,%s %sSprintf的输入格式符号,这个格式符号接受两个参数作为输入,并且在两个参数之间有一个空格。这样就会在两个连结的字符串之间带有一个空格。结果字符串保存在result。这个程序输出如下:

Go is awsome

字符串是不可变的

字符串在Go中是不可变的,一旦创建了字符串,就不可以改变。


package main

import (
	"fmt"
)

func mutate(s string) string {

	s[0] = 'a' // 任何带有单引号的有效的unicode字符是rune

	return s
}

func main() {

	h := "hello"
	fmt.Println(mutate(h))
}

在上面程序中,s[0] = 'a',我们把字符串的第一个字符改变位a,任何带有单引号的有效的unicode字符是rune,我们试着把slice的第0个位置赋值为rune a.这是不允许的,因为字符串是不可改变的,因此,程序会编译失败并且报错 cannot assign to s[0] (strings are immutable)

要解决字符串不可改变的情况,可以把字符串转变成runes类型的切片。需要的时候,切片slice是可变的,修改后,然后再把它转变回字符串。

package main

import (
	"fmt"
)

func mutate(s []rune) string {

	s[0] = 'a' // 任何带有单引号的有效的unicode字符是rune

	return string(s)
}

func main() {

	h := "hello"
	fmt.Println(mutate([]rune(h)))
}

在上面程序中,mutate函数接受一个rune切片作为参数,然后改变切片的第一个元素为a,最后把rune转变成string,并且返回。我们在main函数中调用它,h先是转变成rune类型的切片,然后传递给mutate函数。这个程序的输出如下:

aello

(ps: 今天是旧历2021的最后一天上班,希望来年继续努力,坚持努力,新年快乐!!)

以上就是本篇的全部,感谢阅读,这是我第一次翻译,难免会有翻译不当的地方,如果有什么反馈和评论,欢迎提出来!

原文地址: https://golangbot.com/strings/