Go语言源码剖析-String和unsafe包

简介: Go语言源码剖析-String和unsafe包
版本: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
}


相关文章
|
1天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
1天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
9 2
|
1天前
|
存储 Go
Golang深入浅出之-Go语言依赖管理:GOPATH与Go Modules
【4月更文挑战第27天】Go语言依赖管理从`GOPATH`进化到Go Modules。`GOPATH`时代,项目结构混乱,可通过设置多个工作空间管理。Go Modules自Go 1.11起提供更现代的管理方式,通过`go.mod`文件控制依赖。常见问题包括忘记更新`go.mod`、处理本地依赖和模块私有化,可使用`go mod tidy`、`replace`语句和`go mod vendor`解决。理解并掌握Go Modules对现代Go开发至关重要。
7 2
|
1天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
7 1
|
1天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1
|
1天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
5 0
|
1天前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
5月前
|
编译器 Go
Go 语言基础:包、函数、语句和注释解析
一个 Go 文件包含以下几个部分: 包声明 导入包 函数 语句和表达式 看下面的代码,更好地理解它:
46 0
|
5月前
|
Go
go 包变量函数
go 包变量函数
22 0
|
Java Go Python
Go基础(包、变量和函数):开启Go语言之旅
开启Go语言之旅 Go编程语言是一个开源项目,可以让程序员提高工作效率。 Go是富有表现力,简洁,干净和高效的。其并发机制使编写充分利用多核和联网机器的程序变得容易,而其新颖类型系统则可实现灵活的模块化程序构建。
1387 0