gocookbook
gocookbook copied to clipboard
go cook book
unsafe 包用于编译阶段可以绕过 Go 语言的类型系统,直接操作内存。例如,利用 unsafe 包操作一个结构体的未导出成员。unsafe 包让我可以直接读写内存的能力。 unsafe包只有两个类型,三个函数,但是功能很强大。 ```go type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr ``` `ArbitraryType`是`int`的一个别名,在Go中`ArbitraryType`有特殊的意义。代表一个任意`Go`表达式类型。 `Pointer`是`int`指针类型的一个别名,在Go中可以把任意指针类型转换成`unsafe.Pointer`类型。...
### 函数调用惯例 调用惯例是调用方和被调用方对于参数和返回值传递的约定。 #### C语言的调用惯例 当我们在 x86_64 的机器上使用 C 语言中调用函数时,参数都是通过寄存器和栈传递的,其中: 六个以及六个以下的参数会按照顺序分别使用 edi、esi、edx、ecx、r8d 和 r9d 六个寄存器传递; 六个以上的参数会使用栈传递,函数的参数会以从右到左的顺序依次存入栈中; 而函数的返回值是通过 eax 寄存器进行传递的,由于只使用一个寄存器存储返回值,所以 C 语言的函数不能同时返回多个值。 #### Go语言的调用惯例 Go 通过栈传递函数的参数和返回值,在调用函数之前会在栈上为返回值分配合适的内存空间,随后将入参从右到左按顺序压栈并拷贝参数,返回值会被存储到调用方预留好的栈空间上。 Go 语言的方式能够降低实现的复杂度并支持多返回值,但是牺牲了函数调用的性能; - 不需要考虑超过寄存器数量的参数应该如何传递; -...
### 基础知识 指针保存着一个值的内存地址,类型 `*T`代表指向`T` 类型值的指针。其零值为`nil`。 &操作符为它的操作数生成一个指针。 ```go i := 42 p = &i ``` `*`操作符则会取出指针指向地址的值,这个操作也叫做“解引用”。 ```go fmt.Println(*p) // 通过指针p读取存储的值 *p = 21 // 通过指针p设置p执行的内存地址存储的值 ``` 为什么需要指针类型呢?参考这样一个例子: ``` package main import...
虽然字符串往往被看做一个整体,但是它实际上是一片连续的内存空间,我们也可以将它理解成**一个由字符组成的数组**。C 语言中的字符串使用字符数组 `char[]` 表示。数组会占用一片连续的内存空间,而内存空间存储的字节共同组成了字符串,Go 语言中的字符串只是一个只读的字节数组,下图展示了 `"hello"` 字符串在内存中的存储方式:  Go 语言只是不支持直接修改 `string` 类型变量的内存空间,我们仍然可以通过在 `string` 和 `[]byte` 类型之间反复转换实现修改这一目的: 1. 先将这段内存拷贝到堆或者栈上; 2. 将变量的类型转换成 `[]byte` 后并修改字节数据; 3. 将修改后的字节数组转换回 `string`; `Java`、`Python` 以及很多编程语言的字符串也都是不可变的,这种不可变的特性可以保证我们不会引用到意外发生改变的值。 ### Go语言字符串的结构表示 字符串在...
### 哈希表、Map或者叫映射 哈希表是除了数组之外,最常见的数据结构。几乎所有的语言都会有数组和哈希表两种集合元素,哈希表也被称作字典或者映射。数组用于表示元素的序列,而哈希表示的是键值对之间映射关系。 ### 设计原理 哈希表是计算机科学中的最重要数据结构之一,这不仅因为它 𝑂(1) 的读写性能非常优秀,还因为它提供了键值之间的映射。想要实现一个性能优异的哈希表,需要注意两个关键点 —— 哈希函数和冲突解决方法。 #### 哈希函数 实现哈希表的关键点在于哈希函数的选择,哈希函数的选择在很大程度上能够决定哈希表的读写性能。在理想情况下,哈希函数应该能够将不同键映射到不同的索引上,这要求哈希函数的输出范围大于输入范围,但是由于键的数量会远远大于映射的范围,所以在实际使用时,这个理想的效果是不可能实现的。 比较实际的方式是让哈希函数的结果能够尽可能的均匀分布,然后通过工程上的手段解决哈希碰撞的问题。哈希函数映射的结果一定要尽可能均匀,结果不均匀的哈希函数会带来更多的哈希冲突以及更差的读写性能。 #### 冲突解决 哈希函数输入的范围一定会远远大于输出的范围,所以在使用哈希表时一定会遇到冲突,哪怕我们使用了完美的哈希函数,当输入的键足够多也会产生冲突。所以仍然存在发生哈希碰撞的可能,这时就需要一些方法来解决哈希碰撞的问题,常见方法的就是开放寻址法和拉链法。 > 需要注意的是,这里提到的哈希碰撞不是多个键对应的哈希完全相等,可能是多个哈希的部分相等,例如:两个键对应哈希的前四个字节相同。 #### 拉链法 与开放地址法相比,拉链法是哈希表最常见的实现方法,大多数的编程语言都用拉链法实现哈希表,它的实现比较开放地址法稍微复杂一些,但是平均查找的长度也比较短,各个用于存储节点的内存都是动态申请的,可以节省比较多的存储空间。 拉链法会使用链表数组作为哈希底层的数据结构,我们可以将它看成可以扩展的二维数组。 #### 拉链法写入数据 当我们需要将一个键值对 (Key6, Value6) 写入哈希表时,键值对中的键 Key6...
### 切片是引用类型吗 [数组](https://github.com/kevinyan815/gocookbook/issues/37)需要预先声明长度,有些不灵活,因此在`Go`代码中不经常见到它们。但是切片却无处不在。切片是一段数组的描述符,编译期间的切片是 `slice` 类型的,但是在运行时切片由如下的 `SliceHeader` 结构体表示,其中 `Data` 字段是指向底层数组的指针(可以理解成底层数组中存储切片索引`0`位置上的元素的内存地址),`Len` 表示切片的长度,而 `Cap` 表示切片的容量(最大长度) ```go type SliceHeader struct { Data uintptr Len int Cap int } ``` 切片与底层数组的联系可以用下面这张图表示  很多地方提起切片都会说它是引用类型,但是在上面的`SliceHeader`结构体类型中我们看到切片的属性里只有`Data`是指向底层数组的指针,而长度和容量却不是,这在让我们在平时使用切片时如果稍不注意,尤其是带着在其他语言使用引用类型的思维定式来使用切片时程序不但不会按照预期的运行还会出现一些诡异的现象,我们通过三个例子来看一下。 ```go func...
### 什么是数组 数组是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存来保存其中的元素,我们可以利用数组中元素的索引快速访问特定元素。`Go`语言中我们会使用如下所示的方式来表示数组类型: ``` [10]int [200]interface{} ``` `Go`语言数组在初始化之后大小就无法改变,存储元素类型相同、但是大小不同的数组类型在`Go`语言看来也是完全不同的,只有两个条件都相同才是同一类型。 ``` func NewArray(elem *Type, bound int64) *Type { if bound < 0 { Fatalf("NewArray: invalid bound %v", bound) } t := New(TARRAY)...
`Go`应用程序的初始化是在单一的`goroutine`中执行的。对于包这一级别的初始化来说,**在一个包里会先进行包级别变量的初始化。一个包下可以有多个`init`函数,每个文件也可以有多个`init` 函数,多个 `init` 函数按照它们的文件名顺序逐个初始化。但是程序不可能把所有代码都放在一个包里,通常都是会引入很多包。如果`main`包引入了`pkg1`包,`pkg1`包本身又导入了包`pkg2`,那么应用程序的初始化会按照什么顺序来执行呢? 对于这个初始化过程我粗略的画了一个示意图,理解起来更直观些。  图的上半部分表示了`main`包导入了`pkg1`包,`pkg1`包又导入了`pkg2`包这样一个包之间的依赖关系。图的下班部分表示了,这个应用初始化时初始化工作的执行时间顺序,从被导入的最深层包开始进行初始化,层层递出最后到`main`包,每个包内部的初始化程序依然是先执行包变量初始化再进行`init`函数的执行。 可以通过[这个程序](https://github.com/kevinyan815/gocookbook/tree/master/codes/init_trace)进一步论证上面的结论,程序执行后返回的结果如下,初始化顺序正是和我们预想的一样。 >init P2_v1 : 20 init P2_v2 : 30 init func in pkg2 init P1_v1 : 30 init P1_v2 : 40 init func...
### WaitGroup的使用场景 `WaitGroup`一般是新手接触Go并发编程的第一个原语,这个原语适合用于并发-等待的场景:一个`goroutine`在检查点(Check Point)等待一组执行任务的 worker `goroutine` 全部完成,如果在执行任务的这些worker `goroutine` 还没全部完成,等待的 `goroutine` 就会阻塞在检查点,直到所有woker `goroutine` 都完成后才能继续执行。 `WaitGroup`原语提供三个方法: ```go func (wg *WaitGroup) Add(delta int) func (wg *WaitGroup) Done() func (wg *WaitGroup) Wait() ``` -...
`Go`语言提供的`flag`库支持基础的命令行flag的解析。什么是命令行flag? 举个例子来说,常用的Linux命令`wc -l`这个`-l`就是`wc`命令支持的一个命令行flag。 下面是用`Go`语言的`flag`包编写的一个命令行Demo程序: ``` package main import ( "flag" "fmt" ) func main() { wordPtr := flag.String("word", "Jude", "a string") numbPtr := flag.Int("numb", 42, "an int") boolPtr := flag.Bool("fork",...