版本:go1.14.15
1、字符集
- 计算机里1bit可以是0也可以是1
- 8bit组成1byte,全为0时表示数字0,全为1时表示数字255
- 2byte可以表示65536个数字,更多字节数可以表示更大的数字范围
- 字符如何表示呢?
- ASCII字符集: 其扩展字符集也只有256个(包括英文字母、阿拉伯数字、西文字、控制字符)
- GB2312:包括了简体中文、拉丁字母、日文假名等等
- BIG5:包括了繁体字等
- unicode:于1990年开始研发,1994年正式公布,全球统一化字符编码
2、如何表示混合字符
- 比如"abc字母歌"这种字符如何存储呢?
- 如果直接用unicode字符集表示,如下图
func main() { //string转化为unicode编码切片 str := "hello,world世界" unicode := []rune(str) //打印unicode编码 fmt.Println(unicode) for _, v := range unicode { //打印二进制 fmt.Printf("%b\n", v) } }
- 问题来了
- 这么多二进制编码有长有短,你怎么知道哪个是哪个呢?
- 所以就会有字符边界划分的问题?
- 定长编码可以解决这个问题,但是确实有点浪费空间
- UTF-8编码:它它是一种变长编码,能有效地解决上述问题,可以根据字符大小范围指定字符边界
- 0-127之间的,占用1字节,以0标识在字节开头
- 128-2047之间的,占用2字节,以110和10标识在字节开头
- 2048-65535之间的,占用3字节,分别以1110、10、10标识在字节开头
- 以此类推,更多字节的开头都遵循这样的规则
3、Go语言string
- Go语言默认采用UTF-8编码
package main import "fmt" func main() { //string转化为unicode编码切片 str := "hello,world世界" //打印str的utf-8二进制编码 strSlice := []byte(str) for _, v := range strSlice { fmt.Printf("%b\n", v) } } /* 01101000 - h 01100101 - e 01101100 - l 01101100 - l 01101111 - o 0101100 - , 01110111 - w 01101111 - o 01110010 - r 01101100 - l 01100100 - d 11100100 10111000 10010110 - 世 11100111 10010101 10001100 - 界 */
- Go语言中上述的 str 变量是什么样的结构呢?
- 对于string变量,Go语言认为它不可被修改,所以string变量会记录执行只读字符串的内存起始地址
- 如何找到字符内存地址的结尾地址呢?在C语言中,会在字符的结尾带上\0,但这样就不能写入\0本身这种字符了
- 为了要找到结尾标识,go语言会在变量后面标识只读字符的字节数
- string底层数据结构
type stringStruct struct { str unsafe.Pointer // 底层数组指针 len int // 字符串长度,可以通过 len(string) 返回 }
- 如何修改字符串内容呢?
- slice底层数据结构
type slice struct { array unsafe.Pointer // 底层数组指针,真正存放数据的地方 len int // 切片长度,通过 len(slice) 返回 cap int // 切片容量,通过 cap(slice) 返回 }
- 可以把字符串变量的值赋值给[]byte这样的切片,会给变量从新分配内存,并且会拷贝字符对应的utf-8编码到切片中
- 如何使用实现 []byte 和字符串之间的零拷贝转换?
func StringToBytes(str string) []byte { var b []byte // 切片的底层数组、len字段,指向字符串的底层数组,len字段 *(*string)(unsafe.Pointer(&b)) = str // 切片的 cap 字段赋值为 len(str)的长度,切片的指针、len 字段各占八个字节,直接偏移16个字节 *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*uintptr(8))) = len(str) return b } func BytesToString(data []byte) string { // 直接转换 return *(*string)(unsafe.Pointer(&data)) }
4、什么是unsafe包?
unsafe包是一个特殊的包,提供了一些操作底层数据结构和指针的函数和工具。它允许绕过Go语言类型系统的一些安全限制,直接操作内存和指针。使用unsafe包可以进行一些底层的操作,但同时也增加了代码的不安全性和可移植性问题
- Sizeof:返回一个uintptr类型的值,表示给定变量或类型所占用的字节大小
- Offsetof:返回一个uintptr类型的值,表示给定字段在结构体中的字节偏移量
- Alignof:返回一个uintptr类型的值,表示给定类型的对齐方式(对齐边界)
5、什么内是存对齐?
内存对齐是计算机硬件设计、操作系统和编程语言实现等领域中的一个重要概念。它关系到程序访问内存的效率以及某些硬件的访问规则。
- 基本原则:Go 语言中,结构体的字段顺序可能会影响其内存布局。Go 编译器会自动地进行内存对齐,以优化访问效率。对于结构体的字段,Go 语言有一条规则:结构体的第一个字段的地址就是结构体变量的地址。
- 对齐规则:结构体的每个字段对齐都遵循自身长度的原则,例如 int64 在 64 位系统中将对齐到 8 字节,而 int32 将对齐到 4 字节。
- 内存浪费:如果结构体中的字段顺序不合理,可能会导致内存浪费。例如,如果你首先定义了一个 int32 的字段,然后定义了一个 int64 的字段,因为 int64 需要 8 字节对齐,所以在这两个字段之间会有 4 字节的内存浪费。
- 优化方法:通过合理地排序结构体的字段,可以减少内存的浪费。一般的规则是,将字段按照它们所需对齐字节数的从大到小顺序排列。
- 内存对齐举例1:
下图中对齐边界就是Field2字段大小8字节
package main import ( "fmt" "unsafe" ) type Example struct { Field1 bool // 占用 1 字节 Field2 int64 // 占用 8 字节 Field3 int32 // 占用 4 字节 } func main() { fmt.Println("共占用字节数:", unsafe.Sizeof(Example{})) } //共占用字节数: 24
- 内存对齐举例2:
package main import ( "fmt" "unsafe" ) type ExampleOptimized struct { Field2 int64 // 占用 8 字节 Field3 int32 // 占用 4 字节 Field1 bool // 占用 1 字节 } func main() { fmt.Println("共占用字节数:", unsafe.Sizeof(ExampleOptimized{})) } //共占用字节数: 16
6、unsafe反编译分析
- 示例代码
package main import ( "fmt" "unsafe" ) func main() { a, b, c := UnfafeInfo() fmt.Println(a, b, c) } //go:noinline func UnfafeInfo() (a1 uintptr, a2 uintptr, a3 uintptr) { a1 = unsafe.Sizeof(Student{}) a2 = unsafe.Offsetof(Student{}.Cge) a3 = unsafe.Sizeof(Student{}) return a1, a2, a3 } type Student struct { Age int8 Bge int8 Cge int8 Dge int8 Ege int64 }
- 反编译汇编如下
go build .\s2.3.2.go go tool objdump -S -s "main.UnfafeInfo" .\s2.3.2.exe TEXT main.UnfafeInfo(SB) G:/www/go/hudong/src/test/main/2.3/s2.3.2.go return a1, a2, a3 0x49eb30 48c744240810000000 MOVQ $0x10, 0x8(SP) 0x49eb39 48c744241002000000 MOVQ $0x2, 0x10(SP) 0x49eb42 48c744241810000000 MOVQ $0x10, 0x18(SP) 0x49eb4b c3 RET 0x49eb4c cc INT $0x3 0x49eb4d cc INT $0x3 0x49eb4e cc INT $0x3 0x49eb4f cc INT $0x3
可以从上面汇编代码看出a1、a2、a3三个变量在编译阶段分别接被编译成了立即数$0x10、$0x2、$0x10,可见unsafe并不是一个标准库包,直接操作内存可以写出更高效的代码,Go语言出于安全考虑,把这些对内存的操作放入unsafe包中,从字面起到警示作用
7、uintptr 是什么?
uintptr 是 Go 语言中的一种特殊类型,用于保存指针的数值表示。uintptr 是一个无符号整型,它大到足以存放任何指针的值。它用于进行底层编程,尤其是与操作系统、文件系统等进行交互的时候。在Go语言中对指针的计算是不合法的,但是,Go 提供了 unsafe 包,可以通过这个包进行一些底层的操作在特殊情况下,可能会有所需求。注意,使用 unsafe 包需要特别小心,因为可能会破坏 Go 的类型系统。
- unsafe和uintptr进行指针运算举例:
下面这个例子先把*int转化为无符号的uintptr,然后对它运算,再把结果转化为unsafe.Pointer,最后强制转化为*int,就可以取运算后*int指向的int值
package main import ( "fmt" "unsafe" ) func main() { array := []int{0, 1, 2, 3, 4} first := &array[0] for i := 0; i < len(array); i++ { ithElement := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(first)) + uintptr(i)*unsafe.Sizeof(array[0]))) fmt.Println(ithElement) } } //0 //1 //2 //3 //4
- 使用 unsafe 和 uintptr 操作结构体的内存布局
下面例子首先创建了一个 Person 结构体的实例,并获取了这个结构体开始位置的指针。然后使用 unsafe.Offsetof 函数获取 Age 字段相对于结构体开始位置的偏移量,并通过加法操作得到 Age 字段的指针。最后,把这个指针转换为 *int 类型,来读写 Age 字段
package main import ( "fmt" "unsafe" ) type Person struct { Name string Age int } func main() { p := &Person{Name: "John Doe", Age: 30} namePtr := uintptr(unsafe.Pointer(p)) // Pointer to the start of the struct agePtr := namePtr + unsafe.Offsetof(p.Age) // Pointer to the Age field ageP := (*int)(unsafe.Pointer(agePtr)) // The Age field as an int pointer fmt.Println(*ageP) // Prints: 30 *ageP = 35 // Change the Age field through the pointer fmt.Println(p.Age) // Prints: 35 }
- 使用 unsafe 和 uintptr执行指针类型转换
下面例子创建了一个结构体,然后获取了 b 字段的指针,方法是先获取结构体的指针,然后添加 b 字段的偏移量。将结果转换为 *int16 类型,然后就可以通过这个指针来读写 b 字段
package main import ( "fmt" "unsafe" ) func main() { var x struct { a bool b int16 c []int } // equivalent of pb := &x.b pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) *pb = 42 fmt.Println(x.b) // prints: 42 }