介绍
在 go 语言中的字符串有以下几个特点:
- 字符串是不可变的序列,可以包含任意数据,不过主要是文本。
- 字符串被解读成 utf-8 的序列。
Unicode 编码
计算机一开始,用 ascii 编码即可满足需求,使用 7 位表示 128 个字符,包括大小写字母,英文标点符号、控制符等。
但是,随着互联网的发展,各个国家都要用计算机,ascii 编码已经不够用了,出现了 unicode 编码,而我们熟悉的 UTF-8 UTF-16 UTF-32 是 Unicode 的具体实现 (解决的是怎么存储在计算机)。
Unicode 编码是怎么实现的呢?
基本思路是:把世界上所有能出现的字符,都为其分配一个数字来表示。在 go 语言中,这个标准数字叫做文字符号 rune,计算机中用 int32 类型比较适合保存这个文字符号,所以 rune 就是 int32 的别名,对应的就是 utf-32 的编码。
早期的 unicode 编码为了扩充字符,直接在 ascii 前面加一个字节就好,用两个字节来表示,所以最多容纳 65535 个字符。但是文字非常多,后面把 unicode 编码改成是一个可以变成的字符集,目前规划了U+0000 至 U+10FFFF 为 unicode 编码,占用 21 个 bit,前面增加了 10
,也就十进制的 17,所以是原来的 17 倍,足够用了。
这个 17 个把 unicode 编码分为 17 个平面,每个平面 65535 个字符。由于早期两个字节表示的是第 0 个平面,已经把大量常用的字符给包含在里面了,所以称作基本平面。不在基本平面里的字符都是罕见字符。
对于大于 256 的 unicode 的编码,使用转义字符表示的时候,有两种方式,\uhhhh
和 \Uhhhhhhhh
,小于 256 的可以使用 \x
来表示
\u
开头的表示是用 16 位的码点值\U
开头的表示 32 位的码点值,这几乎用不到
以下几个表示是一样的
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
fmt.Println('世' == '\u4e16') // true
fmt.Println("世" == "\u4e16") // true
Utf-8
Utf-8 是如果实现 unicode 转换呢?
Utf-8 的最大的特点就是变长,节省空间。用 1-4 个字节表示一个字符,和 unicode 的转换是:
- 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。兼容 ascii
- 对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为 0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。
基于以上规则,就可以实现 utf-8 编码到 unicode 编码之间的转换。
Utf-8 编码的汉字确要变成了 3 字节存储,会不会导致空间浪费?
相对于直接用 unicode 编码的 2 字节表示,确实会,不过美国人认为,大部分都是因为,所以英文用 ascii 的话更加节省。
Utf-8 的缺点?
- 无法按下标获取第 n 个字符。
- 需要和 unicode 编码转换,会比较耗费时间
如何获取utf-8编码的字符串长度?
可以使用utf8包来获取
fmt.Println(utf8.RuneCountInString("你好")) // 2
如何变量一个utf8的字符串?
可以使用DecodeRuneInString
s := "你好世界"
for i := 0; i < len(s); {
// 返回utf8的第一个码值,并且返回占用的字节数
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
不过还可以直接使用 range 循环,会按 Utf-8 隐式解码
// i 是按字节的索引位置
for i, r := range "你好世界" {
fmt.Printf("%d\t%c\n", i, r)
}
如不字符串的编码出现问题,导致无法解码,会产生一个unicode编码\uFFFD
替换它,输出的形状是六边形,里面有一个问号。
当对utf8编码的字符串进行[]rune转换的时候,会发生什么?
会返回该字符串的 unicode 的码点序列,注意这个是 unicode 码值,不是 utf8
s := "你好世界"
r := []rune(s)
fmt.Printf("%x", r) // [4f60 597d 554a]
// 文字符号类型的切片,可以转为字符串
fmt.Println(string(r)) // 你好世界
如果将一个整数转为字符串,会把这个整数作为utf-8的码值
fmt.Println(string(65)) //"A",而不是"65"
fmt.Println(string(0x4eac)) //"京"
如果是非法的编码,则会使用编码\uFFFD
替换它
fmt.Println(string(1234567)) //"�"
Utf-16
它和 unicode 的转换比较简单:
- 码位范围是 0x0000~0xFFFF 的字符,只占用 2 个字节,utf-16 用 2 字节表示
- 码位范围是 0x10000~0x10FFFF 的字符,需要占用 4 个字节来表示。Utf-16 用 4 字节表示
Utf -32
UTF-32 是 UNICODE 编码的 4 字节传输格式,每一个传输单元是 4 个字节,前面补零就好。
相关操作
如何获取字符串的字节数量呢?
可以通过 len 函数获取,需要注意的是,这个返回的是字节数量,不是字符数量。
s := "Hello World"
fmt.Println(len(s)) // 11
如何访问因为字符串的中的某个元素呢?
可以通过下标访问,可以获取第 i 个字符,i 大小范围是 0 <= i <= len(s)
,不能超过这个范围,不然会触发宕机异常。需要注意的是,这个是第几个字节,而不是字符,因为 utf-8 编码,一个汉字可能有 3 个字节。
s := "Hello World"
fmt.Printf("%c", s[0]) // H
如何截取字符串的某一部分呢?
s := "Hello World"
fmt.Println(s[:5]) // Hello
fmt.Println(s[6:]) // World
fmt.Println(s[:]) // Hello World
字符串如何拼接呢?
可以使用 +
来拼接字符串。
s := "张三的性别是:"
str := s + "男"
fmt.Println(str) // 张三的性别是:男
拼接的时候有一点要注意,如果字符串太长,需要将+号写在行末,不能写在另外一行的开头,因为 go 会在行末加上分号,如果发现是+号,就不加分号
// 错误的写法
var str = "a"+ "b"
+ "c"
// 正确的写法
var str = "a"+ "b"+
"c"
字符串是如何比较的呢?
字符是通过 == 或 >
来比较的,比较是按字节进行的。
s := "张三的性别是:"
sex := "男"
fmt.Println(sex == s) // false
fmt.Println(sex > s) // true
可以改变字符串里面的内容吗?
不可以,如果去改变的话,会触发编译错误。
s := "Hello World"
s[0] = "1" // cannot assign to s[0] (strings are immutable)
这样的好处是可以安全的共用同一段底层内存:
- 复制长字符串开销很低廉
- 子字符串的生成,开销很低廉
字符串的字面量形式是怎么样的?
通常是用双引号括起来的字符序列,因为 go 源码是 utf-8 的编码,所以字符串一般也是按 utf-8 的编码进行解读的。
s := "Hello 世界"
对于双引号,是会解析特殊字符的,例如以反斜杠开头的转义字符。
s := "Hello 世界 \t"
如果想要字符串的内容原生输出,不被转义,可以使用反引号,需要注意的是:回车符号会被删除,换行符号保留。
s := `Hello 世界
1. 西部世界
`
整数是如何转换成字符串呢?
可以使用 sprintf
来转换,这个比较简单:
x := 123
y := fmt.Sprintf("%d", x) // 双引号括起来
fmt.Println(y)
还可以使用 strconv.Itoa
方法来转换,这个等效 strconv.FormatInt(int64(i), 10)
,就是把数字格式化成 10 进制的一个简写,本身也是返回字符串。
x := 123
fmt.Println(y, strconv.Itoa(x))
还可以使用 strconv.FormatInt
方法来转成字符串,这个方法本身是用于转换成制定进制的数字,只是本身是返回字符串就是了
x := 123
y := strconv.FormatInt(int64(x), 10)
fmt.Printf("%T, %v", y, y) // string, 123
如何将字符串转为整数呢?
可以使用 ParseInt
方法来解析成 int 类型
v32 := "-354634382"
if s, err := strconv.ParseInt(v32, 10, 32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
// 等效 strconv.ParseInt(v32, 10, 0) 0代表是int类型
if s, err := strconv.Atoi(v32); err == nil {
fmt.Printf("%T, %v\n", s, s)
}
对于浮点类型,也可以参考上述方法,strconv 包也有相应的方法进行转换
string 包的一些操作
// 不区分大小比较字符串是否相当
strings.EqualFold("Go", "go")// true
// 是否包含
strings.Contains("seafood", "foo") // true
// 分割
res := strings.Split("http://www.pdfqupu.com", "://")
res := strings.SplitN("http://www.pdfqupu.com", "://", 2) // 指定分割两个
return res[0], res[1]
// 结合,将切片结合在一起
t = strings.Join(numStrList, ":")
// 字符串出现的位置,不存在返回-1,应该?
splitCharIndex := strings.Index(sheetName, "-")
// 去除头尾空格
strings.TrimSpace(c.rawData)
// 是否包含前缀
strings.HasPrefix(sheetName, "指定区服排期")
// 替换所有字符
strings.ReplaceAll(timeStr, ":", ":")
// 不存在返回-1,有的话,返回第一个
strings.Index(sheetName, "-")