- Golang 官方文档: golang.google.cn
- C 语言中文网: Golang 简介
字符串基础
一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时占用一个字节,其它字符根据需要占用 2-4 个字节)
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入的讲,字符串是字节的定长数组。
定义字符串
可以使用双引号 "
来定义字符串,字符串可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:
\n
: 换行符\r
: 回车符\t
: tab 键\\
: 反斜杠自身\u
或\U
: Unicode 字符
示例1:
1
2
3
4
5
6
7
8
9
10
11
12package main
import "fmt"
func main() {
var str = "www.59izt.com\nGo语言教程"
fmt.Println(str)
}
// 运行结果为
www.59izt.com
Go语言教程
字符串比较
一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过 len()
方法来获取,例如: len(str)
。
字符串索引
字符串的内容(纯字节)可以通过标准索引法来获取,在方括号 []
内 写入索引,索引从 0 开始计数:
- 字符串 str 的第一个字节:
str[0]
- 字符串 str 的第 i 个字节:
str[i-1]
- 字符串 str 的最后一个字节:
str[len(str)-1]
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效
注意: 获取字符串中某个字节的地址属于非法行为,例如:
&str[i]
字符串拼接(“+”)
两个字符串 s1 和 s2 可以通过 s := s1 + s2
拼接在一起,将 s2 追加到 s1 的尾部并生成一个新的字符串 s。
示例1: 可以通过下面的方式来对代码中多行的字符串进行拼接
1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
str1 := "Beginning of the string, " +
"second part of the string"
fmt.Println(str1)
}输入结果如下:
1
Beginning of the string, second part of the string
提示: 因为编译器会在行尾自动补全分号,所以拼接字符串用的加号
+
必须放在第一行末尾。示例2: 也可以使用
+=
来对字符串进行拼接1
2
3
4
5
6
7
8
9package main
import "fmt"
func main() {
s := "Hel" + "lo,"
s += "World!"
fmt.Println(s)
}输出结果如下:
1
Hello,World!
字符串多行
在 go 语言中,使用双引号 "
书写字符串是字符串最常见的表达方式之一,被成为 字符串字面量(string literal)
,这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用反引号(`)。
示例1: 定义一个跨多行的常量 str
1
2
3
4
5
6
7
8
9const str = `第一行
第二行
第三行
\r\n
`
func main() {
fmt.Println(str)
}运行结果如下:
1
2
3
4第一行
第二行
第三行
\r\n反引号(`) 是键盘上数 1 键左边的键,两个反引号之间的字符串将被原样赋值到 str 变量中.
在这种方式下,反引号之间的换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
多行字符串一般用于内嵌源码和内嵌数据等,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26const codeTemplate = `// Generated by github.com/davyxu/cellnet/
protoc-gen-msg
// DO NOT EDIT!{{range .Protos}}
// Source: {{.Name}}{{end}}
package {{.PackageName}}
{{if gt .TotalMessages 0}}
import (
"github.com/davyxu/cellnet"
"reflect"
_ "github.com/davyxu/cellnet/codec/pb"
)
{{end}}
func init() {
{{range .Protos}}
// {{.Name}}{{range .Messages}}
cellnet.RegisterMessageMeta("pb","{{.FullName}}", reflect.TypeOf((*{{.Name}})(nil)).Elem(), {{.MsgID}}) {{end}}
{{end}}
}
`
func main() {
fmt.Println(codeTemplate)
}这段代码只定义了一个常量
codeTemplate
,类型为字符串,使用反引号定义,字符串内的内容为一段代码生成中使用到的 Go 源码格式。在反引号之间的所有代码均不会被编译器识别,而只是作为字符串的一部分。
字符串常用方法
字符串类型在业务中的应用可以说是最广泛的,我们需要详细了解字符串的常见用法,如下
计算字符串的长度
go 语言中的内建函数
len()
,可以用来获取切片,字符串,通道(channel)等数据类型的长度。- 下面的代码可以用
len()
来获取字符串的长度
1
2
3
4
5
6
7func main() {
tip1 := "genji is a ninja"
fmt.Println(len(tip1))
tip2 := "忍者"
fmt.Println(len(tip2))
}程序输出如下
1
216
6len()
函数的返回值类型为 int,表示字符串的 ASCII 字符个数或字节长度- 输出中第一行的 16 表示 tip1 的字符个数为16
- 输出中第二行的 6 表示 tip2 的字符个数,也就是 “忍者” 的字符个数是 6,然而根据习惯,”忍者” 的字符个数应该是 2.
这里的差异是由于 go 语言的字符串都是以 UTF-8 格式保存,每个中文占用3个字节,因此使用
len()
获取两个中文文字对应的字节数为 6.- 下面的代码可以用
如果希望按习惯上的字符个数来计算,就需要使用 go 语言中 UTF-8 包提供的
RuneCountInString()
函数,统计 Unicode 字符数量。- 下面的代码展示如何计算 UTF-8 的字符个数
1
2
3
4
5
6
7
8
9
10
11package main
import (
"fmt"
"unicode/utf8"
)
func main() {
fmt.Println(utf8.RuneCountInString("忍者"))
fmt.Println(utf8.RuneCountInString("龙忍出鞘,fight!"))
}程序输出如下
1
22
11一般游戏中在登录时都要输入名字,而名字一般有长度限制。考虑到国人习惯使用中文做名字,就需要检测字符串 UTF-8 格式的长度。
总结:
- ASCII 字符串长度使用
len()
函数。- Unicode 字符串长度使用
utf8.RuneCountInString()
函数。
遍历字符串
遍历字符串有下面两种写法
遍历每一个 ASCII 字符,遍历 ASCII 字符使用 for 的数值循环进行遍历,直接取每个字符串的下标获取 ASCII 字符,如下面的例子所示
1
2
3
4
5
6
7func main() {
theme := "狙击 start"
for i := 0; i < len(theme); i++ {
fmt.Printf("ascii: %c %d\n", theme[i], theme[i])
}
}程序输出如下
1
2
3
4
5
6
7
8
9
10
11
12ascii: ç 231
ascii: 139
ascii: 153
ascii: å 229
ascii: 135
ascii: » 187
ascii: 32
ascii: s 115
ascii: t 116
ascii: a 97
ascii: r 114
ascii: t 116这种模式下取到的汉字 “惨不忍睹”。由于没有使用 Unicode,汉字被显示为乱码。
按 Unicode 字符遍历字符串,同样的内容
1
2
3
4
5
6
7func main() {
theme := "狙击 start"
for _, s := range theme {
fmt.Printf("Unicode: %c %d\n", s, s)
}
}程序输出如下
1
2
3
4
5
6
7
8Unicode: 狙 29401
Unicode: 击 20987
Unicode: 32
Unicode: s 115
Unicode: t 116
Unicode: a 97
Unicode: r 114
Unicode: t 116可以看到,这次汉字可以正常输出了
总结:
- ASCII 字符串遍历直接使用下标。
- Unicode 字符串遍历使用
for range
。
截取字符串
获取字符串的某一段字符是开发中常见的操作,我们一般将字符串中的某一段字符称做子串(substring)。
下面的例子中使用
string.Index()
函数在字符串中搜索另外一个子串,代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main
import (
"fmt"
"strings"
)
func main() {
tracer := "死神来了,死神bye bye"
comma := strings.Index(tracer, ",")
pos := strings.Index(tracer[comma:], "死神")
fmt.Println(comma, pos, tracer[comma+pos:])
}程序输出如下
1
12 3 死神bye bye
代码说明如下
- 第 2 行尝试在 tracer 的字符串中搜索中文的逗号,返回的位置存在 comma 变量中,类型是 int,表示从 tracer 字符串开始的 ASCII 码位置。
strings.Index()
函数并没有像其他语言一样,提供一个从某偏移开始搜索的功能。不过我们可以对字符串进行切片操作来实现这个逻辑。 - 第4行中,
tracer[comma:]
从 tracer 的 comma 位置开始到 tracer 字符串的结尾构造一个子字符串,返回给string.Index()
进行再索引。得到的 pos 是相对于tracer[comma:]
的结果。 - comma 逗号的位置是 12,而 pos 是相对位置,值为 3。我们为了获得第二个“死神”的位置,也就是逗号后面的字符串,就必须让 comma 加上 pos 的相对偏移,计算出 15 的偏移,然后再通过切片
tracer[comma+pos:]
计算出最终的子串,获得最终的结果:“死神bye bye”。
- 第 2 行尝试在 tracer 的字符串中搜索中文的逗号,返回的位置存在 comma 变量中,类型是 int,表示从 tracer 字符串开始的 ASCII 码位置。
总结:
字符串索引比较常用的有如下几种方法:
- strings.Index:正向搜索子字符串。
- strings.LastIndex:反向搜索子字符串。
- 搜索的起始位置可以通过切片偏移制作。
修改字符串
Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。
示例1:
1
2
3
4
5
6
7
8
9func main() {
angel := "Heros never die"
angelBytes := []byte(angel)
for i := 5; i <= 10; i++ {
angelBytes[i] = ' '
}
fmt.Println(string(angelBytes))
}程序输出如下
1
Heros die
代码说明:
- 第 3 行中,将字符串转换为字符串数组
- 第 5~7 行利用循环,将 never 单词替换为空格
- 最后打印结果
感觉我们通过代码达成了修改字符串的过程,但真实的情况是:Go 语言中的字符串和其他高级语言(Java、C#)一样,默认是不可变的(immutable)。
字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。所以说,代码中实际修改的是
[]byte
,[]byte
在 Go 语言中是可变的,本身就是一个切片。在完成了对[]byte
操作后,在第 9 行,使用string()
将[]byte
转为字符串时,重新创造了一个新的字符串。
总结:
- Go 语言的字符串是不可变的。
- 修改字符串时,可以将字符串转换为
[]byte
进行修改。[]byte
和string
可以通过强制类型转换互转。
拼接字符串
连接字符串这么简单,还需要学吗?确实,Go 语言和大多数其他语言一样,使用加号 +
对字符串进行连接操作,非常直观。除了加号连接字符串,Go 语言中也有类似于 StringBuilder
的机制来进行高效的字符串连接
示例:
1
2
3
4
5
6
7
8
9
10
11func main() {
hammer := "吃我一锤"
sickle := "死吧"
// 声明字节缓冲
var stringBuilder bytes.Buffer
stringBuilder.WriteString(hammer)
stringBuilder.WriteString(sickle)
fmt.Println(stringBuilder.String())
}bytes.Buffer
是可以缓冲并可以往里面写入各种字节数组的。字符串也是一种字节数组,使用WriteString()
方法进行写入。
将需要连接的字符串,通过调用WriteString()
方法,写入stringBuilder
中,然后再通过stringBuilder.String()
方法将缓冲转换为字符串。
格式化输出
格式化在逻辑中非常常用。使用格式化函数,要注意写法:
1 | fmt.Sprintf(格式化样式, 参数列表…) |
说明:
- 格式化样式:字符串形式,格式化动词以%开头。
- 参数列表:多个参数以逗号分隔,个数必须与格式化样式中的个数一一对应,否则运行时会报错。
示例: 在 Go 语言中,格式化的命名延续C语言风格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26func main() {
var progress = 2
var target = 8
// 两参数格式化
title := fmt.Sprintf("已采集 %d 个药草,还需要 %d 个完成任务", progress, target)
fmt.Println(title)
pi := 3.14159
// 按数值本身的格式输出
variant := fmt.Sprintf("%v %v %v", "月球基地", pi, true)
fmt.Println(variant)
// 匿名结构体声明并赋予初值
profile := &struct {
Name string
HP int
}{
Name: "rat",
HP: 150,
}
fmt.Printf("使用 '%%+v' %+v\n", profile)
fmt.Printf("使用 '%%#v' %#v\n", profile)
fmt.Printf("使用 %%T %T\n", profile)
}代码输出如下
1
2
3
4
5已采集 2 个药草,还需要 8 个完成任务
月球基地 3.14159 true
使用 '%+v' &{Name:rat HP:150}
使用 '%#v' &struct { Name string; HP int }{Name:"rat", HP:150}
使用 %T *struct { Name string; HP int }下表中标出了常用的一些格式化样式中的动词及功能。
格式化样式 功能 %v
按值的本来值输出 %+v
在 %v
的基础上,对结构体字段名和值进行展开%#v
输出 go 语言语法格式的值 %T
输出 go 语言语法格式的类型和值 %%
输出 %
本身%b
整形以二进制方式显示 %o
整形以八进制方式显示 %d
整形以十进制方式显示 %x
整形以十六进制方式显示 %X
整形以十六进制,字母大写方式显示 %U
Unicode 字符 %f
浮点数 %p
指针,十六进制方式显示
字符类型
字符串中的每一个元素叫做 ”字符“,在遍历或单个获取字符串元素时可以获得字符。
go 语言的字符有以下两种:
- 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
- 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
byte 类型是 unit8 的别名,对于只占用1个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = 'A'
,字符使用单引号括起来。
在 ASCII 码表中,A 的值是 65,使用 16 进制表示则为 41,所以下面的写法是等效的:
1
2
3var ch byte = 65
// 或
var ch byte = '\x41' //(\x 总是紧跟着长度为 2 的 16 进制数)另外一种可能的写法是
\
后面紧跟着长度为 3 的八进制数,例如\377
。
Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。
在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。
例如:
1
2
3
4
5
6
7
8
9
10func main() {
var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // interger
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U\n", ch, ch2, ch3) // UTF-8 code point
}程序输出如下
1
2
3
465 - 946 - 1053236
A - β -
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234格式化说明符
%c
用于表示字符,当和字符配合使用时,%v
或%d
会输出用于表示该字符的整数,%U
输出格式为U+hhhh
的字符串。
Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值,如下所示(其中 ch 代表字符)
- 判断是否为字母:
unicode.IsLetter(ch)
- 判断是否为数字:
unicode.IsDigit(ch)
- 判断是否为空白符号:
unicode.IsSpace(ch)
UTF-8 和 Unicode 有何区别
Unicode 与 ASCII 类似,都是一种字符集。
字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。
UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:
- 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
- 从 128 到 0x10ffff 表示其他字符。
根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。
广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。