Go语言之字符串八
字符串在 Go 语言中以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。
字符串的值为双引号中的内容,可以在 Go 语言的源码中直接添加非 ASCII 码字符,代码如下:
str := "hello world"
ch := "中文"
字符串转义符
Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
转移符 含 义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
' 单引号
" 双引号
\ 反斜杠
在 Go 语言源码中使用转义符代码如下:
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Go\\bin\\go.exe\"")
}
代码运行结果:
str := "c:\Go\bin\go.exe"
这段代码中将双引号和反斜杠“\”进行转义。
字符串实现基于 UTF-8 编码
Go 语言里的字符串的内部实现使用 UTF-8 编码。通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。当然,Go 语言也支持按传统的 ASCII 码方式进行逐字符访问。
定义多行字符串
在源码中,将字符串的值以双引号书写的方式是字符串的常见表达方式,被称为字符串字面量(string literal)。这种双引号字面量不能跨行。如果需要在源码中嵌入一个多行字符串时,就必须使用`字符,代码如下:
const str = ` 第一行
第二行
第三行
\r\n
`
fmt.Println(str)
代码运行结果:
第一行
第二行
第三行
\r\n
`叫反引号,就是键盘上 1 键左边的键,两个反引号间的字符串将被原样赋值到 str 变量中。
在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
const 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}}
}
`
这段代码只定义了一个常量 codeTemplate,类型为字符串,使用`定义。字符串的内容为一段代码生成中使用到的 Go 源码格式。
在`间的所有代码均不会被编译器识别,而只是作为字符串的一部分。
字符串的长度
Go 语言的内建函数 len(),可以用来获取切片、字符串、通道(channel)等的长度。下面的代码可以用 len() 来获取字符串的长度。
tip1 := "genji is a ninja"
fmt.Println(len(tip1))
tip2 := "忍者" //一个中文是3个字符
fmt.Println(len(tip2))
程序输出如下:
16
6
len() 函数的返回值的类型为 int,表示字符串的 ASCII 字符个数或字节长度。
- 输出中第一行的 16 表示 tip1 的字符个数为 16。
- 输出中第二行的 6 表示 tip2 的字符格式,也就是“忍者”的字符个数是 6,然而根据习惯,“忍者”的字符个数应该是 2。
这里的差异是由于 Go 语言的字符串都以 UTF-8 格式保存,每个中文占用 3 个字节,因此使用 len() 获得两个中文文字对应的 6 个字节。
如果希望按习惯上的字符个数来计算,就需要使用 Go 语言中 UTF-8 包提供的 RuneCountInString() 函数,统计 Uncode 字符数量。
下面的代码展示如何计算UTF-8的字符个数。
fmt.Println(utf8.RuneCountInString("忍者"))
fmt.Println(utf8.RuneCountInString("龙忍出鞘,fight!"))
程序输出如下:
2
11
一般游戏中在登录时都需要输入名字,而名字一般有长度限制。考虑到国人习惯使用中文做名字,就需要检测字符串 UTF-8 格式的长度。
总结
ASCII 字符串长度使用 len() 函数。
Unicode 字符串长度使用 utf8.RuneCountInString() 函数。
字符串的fmt.Sprintf(格式化输出)
格式化在逻辑中非常常用。使用格式化函数,要注意写法:
fmt.Sprintf(格式化样式, 参数列表…)
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)
代码输出如下:
已采集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 }C语言中, 使用%d代表整型参数
下表中标出了常用的一些格式化样式中的动词及功能。
表:字符串格式化时常用动词及功能
动 词 功 能
%d int变量
%x, %o, %b 分别为16进制,8进制,2进制形式的int
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔变量:true 或 false
%c rune (Unicode码点),Go语言里特有的Unicode字符类型
%s string
%q 带双引号的字符串 "abc" 或 带单引号的 rune 'c'
%v 会将任意变量以易读的形式打印出来
%T 打印变量的类型
%% 字符型百分比标志(%符号本身,没有其他操作)
遍历字符串——获取每一个字符串元素
遍历字符串有下面两种写法。
遍历每一个ASCII字符
遍历 ASCII 字符使用 for 的数值循环进行遍历,直接取每个字符串的下标获取 ASCII 字符,如下面的例子所示。
theme := "狙击 start"
for i := 0; i < len(theme); i++ {
fmt.Printf("ascii: %c %d\n", theme[i], theme[i])
}
程序输出如下:
ascii: ? 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字符遍历字符串
同样的内容:
theme := "狙击 start"
for _, s := range theme {
fmt.Printf("Unicode: %c %d\n", s, s)
}
程序输出如下:
Unicode: 狙 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)。
下面例子中使用 strings.Index() 函数在字符串中搜索另外一个子串,代码如下:
tracer := "死神来了, 死神bye bye"
comma := strings.Index(tracer, ", ")
pos := strings.Index(tracer[comma:], "死神")
fmt.Println(comma, pos, tracer[comma+pos:])
程序输出如下:
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”。
总结
字符串索引比较常用的有如下几种方法:
- strings.Index:正向搜索子字符串。
- strings.LastIndex:反向搜索子字符串。
- 搜索的起始位置可以通过切片偏移制作。
修改字符串
Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。请参考下面的代码:
angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))
程序输出如下:
Heros die
代码说明如下:
- 在第 2 行中,将字符串转为字符串数组。
- 第 3~6 行利用循环,将 never 单词替换为空格。
最后打印结果。
感觉我们通过代码达成了修改字符串的过程,但真实的情况是:Go 语言中的字符串和其他高级语言(Java、C#)一样,默认是不可变的(immutable)。
字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。
所以说,代码中实际修改的是 []byte,[]byte 在 Go 语言中是可变的,本身就是一个切片。
在完成了对 []byte 操作后,在第 9 行,使用 string() 将 []byte 转为字符串时,重新创造了一个新的字符串。
总结
- Go 语言的字符串是不可变的。
- 修改字符串时,可以将字符串转换为 []byte 进行修改。
- []byte 和 string 可以通过强制类型转换互转。
字符串拼接(连接)
连接字符串这么简单,还需要学吗?确实,Go 语言和大多数其他语言一样,使用+
对字符串进行连接操作,非常直观。
但问题来了,好的事物并非完美,简单的东西未必高效。除了加号连接字符串,Go 语言中也有类似于 StringBuilder 的机制来进行高效的字符串连接,例如:
hammer := "吃我一锤"
sickle := "死吧"
// 声明字节缓冲
var stringBuilder bytes.Buffer
// 把字符串写入缓冲
stringBuilder.WriteString(hammer)
stringBuilder.WriteString(sickle)
// 将缓冲以字符串形式输出
fmt.Println(stringBuilder.String())
bytes.Buffer 是可以缓冲并可以往里面写入各种字节数组的。字符串也是一种字节数组,使用 WriteString() 方法进行写入。
将需要连接的字符串,通过调用 WriteString() 方法,写入 stringBuilder 中,然后再通过 stringBuilder.String() 方法将缓冲转换为字符串。
Base64编码——电子邮件的基础编码格式
Base64 编码是常见的对 8 比特字节码的编码方式之一。Base64 可以使用 64 个可打印字符来表示二进制数据,电子邮件就是使用这种编码。
Go 语言的标准库自带了 Base64 编码算法,通过几行代码就可以对数据进行编码,示例代码如下。
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// 需要处理的字符串
message := "Away from keyboard. https://golang.org/"
// 编码消息
encodedMessage := base64.StdEncoding.EncodeToString([]byte (message))
// 输出编码完成的消息
fmt.Println(encodedMessage)
// 解码消息
data, err := base64.StdEncoding.DecodeString(encodedMessage)
// 出错处理
if err != nil {
fmt.Println(err)
} else {
// 打印解码完成的数据
fmt.Println(string(data))
}
}
代码说明如下:
第 11 行为需要编码的消息,消息可以是字符串,也可以是二进制数据。
第 14 行,base64 包有多种编码方法,这里使用 base64.StdEnoding 的标准编码方法进行编码。传入的字符串需要转换为字节数组才能供这个函数使用。
第 17 行,编码完成后一定会输出字符串类型,打印输出。
第 20 行,解码时可能会发生错误,使用 err 变量接收错误。
第 24 行,出错时,打印错误。
第 27 行,正确时,将返回的字节数组([]byte)转换为字符串。
本文学习来源于C语言中文网>Go语言教程