《快学 Go 语言》第 7 课 —— 字符串

字符串一般有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每一个字都是定长的,而「字节」串中每一个字是不定长的。Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字节。这意味着没法经过位置来快速定位出一个完整的字符来,而必须经过遍历的方式来逐个获取单个字符。编程

咱们所说的字符一般是指 unicode 字符,你能够认为全部的英文和汉字在 unicode 字符集中都有一个惟一的整数编号,一个 unicode 一般用 4 个字节来表示,对应的 Go 语言中的字符 rune 占 4 个字节。在 Go 语言的源码中能够找到下面这行代码,rune 类型是一个衍生类型,它在内存里面使用 int32 类型的 4 个字节存储。数组

type rune int32
复制代码

使用「字符」串来表示字符串势必会浪费空间,由于全部的英文字符原本只须要 1 个字节来表示,用 rune 字符来表示的话那么剩余的 3 个字节都是零。可是「字符」串有一个好处,那就是能够快速定位。bash

为了进一步方便读者理解字节 byte 和 字符 rune 的关系,我花了下面这张图网络

其中 codepoint 是每一个「字」的其实偏移量。Go 语言的字符串采用 utf8 编码,中文汉字一般须要占用 3 个字节,英文只须要 1 个字节。len() 函数获得的是字节的数量,经过下标来访问字符串获得的是「字节」。函数

按字节遍历

字符串能够经过下标来访问内部字节数组具体位置上的字节,字节是 byte 类型ui

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for i:=0;i<len(s);i++ {
		fmt.Printf("%x ", s[i])
	}
 
}

-----------
e5 98 bb e5 93 88 63 68 69 6e 61
复制代码

按字符 rune 遍历

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for codepoint, runeValue := range s {
		fmt.Printf("%d %d ", codepoint, int32(runeValue))
	}
}

-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
复制代码

对字符串进行 range 遍历,每次迭代出两个变量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示对应的 unicode 编码(类型是 rune)。编码

字节串的内存表示

若是字符串仅仅是字节数组,那字符串的长度信息是怎么获得呢?要是字符串都是字面量的话,长度尚能够在编译期计算出来,可是若是字符串是运行时构造的,那长度又是如何获得的呢?spa

var s1 = "hello" // 静态字面量
var s2 = ""
for i:=0;i<10;i++ {
  s2 += s1 // 动态构造
}
fmt.Println(len(s1))
fmt.Println(len(s2))
复制代码

为解释这点,就必须了解字符串的内存结构,它不单单是前面提到的那个字节数组,编译器还为它分配了头部字段来存储长度信息和指向底层字节数组的指针,图示以下,结构很是相似于切片,区别是头部少了一个容量字段。设计

当咱们将一个字符串变量赋值给另外一个字符串变量时,底层的字节数组是共享的,它只是浅拷贝了头部字段。指针

字符串是只读的

你可使用下标来读取字符串指定位置的字节,可是你没法修改这个位置上的字节内容。若是你尝试使用下标赋值,编译器在语法上直接拒绝你。

package main

func main() {
	var s = "hello"
	s[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]
复制代码

切割切割

字符串在内存形式上比较接近于切片,它也能够像切片同样进行切割来获取子串。子串和母串共享底层字节数组。

package main

import "fmt"

func main() {
	var s1 = "hello world"
	var s2 = s1[3:8]
	fmt.Println(s2)
}

-------
lo wo

复制代码

字节切片和字符串的相互转换

在使用 Go 语言进行网络编程时,常常须要未来自网络的字节流转换成内存字符串,同时也须要将内存字符串转换成网络字节流。Go 语言直接内置了字节切片和字符串的相互转换语法。

package main

import "fmt"

func main() {
	var s1 = "hello world"
	var b = []byte(s1)  // 字符串转字节切片
	var s2 = string(b)  // 字节切片转字符串
	fmt.Println(b)
	fmt.Println(s2)
}

--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world
复制代码

从节省内存的角度出发,你可能会认为字节切片和字符串的底层字节数组是共享的。可是事实不是这样的,底层字节数组会被拷贝。若是内容很大,那么转换操做是须要必定成本的。

那为何须要拷贝呢?由于字节切片的底层数组内容是能够修改的,而字符串的底层字节数组是只读的,若是共享了,就会致使字符串的只读属性再也不成立。

阅读《快学 Go 语言》更多章节,关注公众号「码洞」

相关文章
相关标签/搜索