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
}


相关文章
|
2天前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
4天前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
19 4
|
4天前
|
缓存 监控 前端开发
在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统
本文深入探讨了在 Go 语言中实现 WebSocket 实时通信的应用,包括 WebSocket 的简介、Go 语言的优势、基本实现步骤、应用案例、注意事项及性能优化策略,旨在帮助开发者构建高效稳定的实时通信系统。
33 1
|
8天前
|
Go
go语言中的continue 语句
go语言中的continue 语句
18 3
|
8天前
|
安全 Go 调度
探索Go语言的并发模型:goroutine与channel
在这个快节奏的技术世界中,Go语言以其简洁的并发模型脱颖而出。本文将带你深入了解Go语言的goroutine和channel,这两个核心特性如何协同工作,以实现高效、简洁的并发编程。
|
10天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
44 1
|
3天前
|
存储 Go PHP
Go语言中的加解密利器:go-crypto库全解析
在软件开发中,数据安全和隐私保护至关重要。`go-crypto` 是一个专为 Golang 设计的加密解密工具库,支持 AES 和 RSA 等加密算法,帮助开发者轻松实现数据的加密和解密,保障数据传输和存储的安全性。本文将详细介绍 `go-crypto` 的安装、特性及应用实例。
13 0
|
11天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
31 2
|
存储 JavaScript 前端开发
go源码解析-Println的故事
本文主要通过平常常用的go的一个函数,深入源码,了解其底层到底是如何实现的。 Println Println函数接受参数a,其类型为…interface{}。用过Java的对这个应该比较熟悉,Java中也有…的用法。
|
10天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
20 2