浅析 GO 中的内存对齐

浅析 GO 中的内存对齐 前置概念 位(bit) ​ 所谓位,是最基本的概念,在计算机中,由于只有逻辑0和逻辑1的存在,因此很多东西、动作、数字都要表示为一串二进制的字码。例如: 1001 0000 1101等等。其中每一个逻辑0或者1便是一个位。例如这个例子里的1000 1110共有八个位,它的英文名字叫(bit),是计算机中最基本的单位。 字节(Byte) ​ 由八个位(bit)组成的一个单元,也就是8个bit组成1个Byte。在计算机科学中,用于表示ASCII字符,便是运用字节来记录表示字母和一些符号,例如字符A便用 “0100 0001”来表示。 字(word) 表示被处理信息的单位,用来度量数据类型的宽度。 ​ 在计算机体系结构中,“字"是处理器可以在单个操作中处理的数据单元 - 通常是内存中可寻址的最小单位。它是固定大小的比特(二进制数字)组。处理器的字长决定了它处理数据的效率。常见的字长包括 8、16、32 和 64 比特。一些计算机处理器体系结构支持半字,即一个字中的一半比特数,以及双字,即两个相邻字。 ​ 现在最常见的架构是 32 位和 64 位。如果你有 32 位处理器,那意味着它可以一次访问 4 个字节,也就是字长为 4 个字节。如果你有 64 位处理器,那意味着它可以一次访问 8 个字节,也就是字长为 8 个字节。 ​ 将数据存储在内存中时,每个 32 位数据字都有一个唯一地址,如下所示。 Figure. 1 - 可寻址内存 ​ 我们可以使用加载字(lw)指令读取存储在内存中的数据并将其加载到一个寄存器中。 ​ 字的位数并不是确定值,如 x86 机器将字定义为16位(汇编语言课程中),也就是两个字节,在32位arm机器中,字定义为32位(嵌入式课程中)。 ​ 指令字长:字节的整数倍,指一个指令字中包含的二进制代码位数。 ​ 存储字长:字节的整数倍,一个存储单元存储的二进制代码的长度。 字是单位,随系统而变,字长是同一时间处理二进制的长度。 字符与字节对应关系 ​ 常见的编码字符与字节的对应关系如下: ① ASCII 码中,一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值0,最大值255。 ② UTF-8 编码中,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。 ③ Unicode 编码中,一个英文等于两个字节,一个中文(含繁体)等于两个字节。 符号:英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占1个字节的大小,中文句号“。”占2个字节的大小。 ④ GBK 编码方式是中文占两个字节,英文占1个字节。 ...

2024-12-12 · 6 min · 2813 words · Luenci

Golang pprof 性能分析指南

Golang pprof 性能分析指南 pprof 是一个用于可视化和分析分析数据的工具。 采样方式 方式名称 如何使用 优点 缺点 使用场景 runtime/pprof 手动调用【runtime.StartCPUProfile、runtime.SweightCPUProfile】等API来进行数据的采集。采集程序(非 Server)的指定区块的运行数据进行分析。 灵活性高、按需采集。 工具型应用(比如说定制化的分析小工具、集成到公司监控系统)。这种应用运行一段时间就结束。 net/http/pprof 通过http服务来获取Profile采样文件。 import _ "net/http/pprof"。基于 HTTP Server 运行,并且可以采集运行时数据进行分析。net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来 简单易用 在线服务(一直运行着的程序) go test 通过命令go test -bench . -cpuprofile cpu.prof来进行采集数据。 针对性强、细化到函数 进行某函数的性能测试 指标解释 常用指标如下: allocs:所有时刻的内存使用情况,包括正在使用的及已经回收的 block:导致在同步原语上发生阻塞的堆栈跟踪 cmdline: 当前程序的命令行的完整调用路径。 goroutine:目前的 goroutine 数量及运行情况 heap:当前时刻的内存使用情况 mutex:查看导致互斥锁的竞争持有者的堆栈跟踪 profile:默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件 threadcreate:查看创建新 OS 线程的堆栈跟踪。 trance:当前程序执行的追踪,可以在秒数的 GET 参数中指定持续时间。在获取追踪文件后,请使用 go 工具的 trace 命令来调查追踪。(深入浅出 Go trace (qq.com)) 注意,默认情况下是不追踪block和mutex的信息的,如果想要看这两个信息,需要在代码中加上两行: 1 2 runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex 注意,上文的所有信息都是实时的,如果你刷新一下,是可以看到数字在变化的。此时如果点击蓝色的连接,可以看到一些协程的栈信息,这些信息并不容易阅读。如果想要更加清晰的数据,需要将信息保存下来,在本地进行分析。 ...

2024-08-03 · 7 min · 3029 words · Luenci

Go面向对象相关知识

Go面向对象相关知识 类的封装和绑定方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package main import "fmt" // go语言没有class关键字生成类 // 使用struct声明类 type Person struct { // 成员属性 name string age int gender string } // 类外边绑定方法 // 类的方法,可以使用自己的成员 // 使用指针可以修改类的成员变量等 func (p *Person) Eat() { fmt.Println("使用 *Person 指针 修改前") fmt.Println(p.name + " is eating") p.name = "luenci" fmt.Println("使用 *Person 指针 修改后") fmt.Println(p.name + " is eating") } func (p Person) Eat2() { fmt.Println("使用 Person 不是指针 修改前") fmt.Println(p.name + " is eating") p.name = "luenci" fmt.Println("使用 Person 不是指针 修改后") fmt.Println(p.name + " is eating") } func main() { lynn := Person{ name: "lynn", age: 20, gender: "girl", } lynn.Eat() lynn.Eat2() } 类的继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package main import "fmt" type Human struct { name string sex string age int } type Student struct { hum Human // 包含 Human类型的变量 是嵌套类 school string } type Teacher struct { Human // 直接声明Human类型,没有定义变量 类继承 school string } // 类外面绑定方法 func (h *Human) Eat() { fmt.Println(h.name + " is eating") } func main() { st1 := Student{ hum: Human{ name: "lynn", sex: "girl", age: 20, }, school: "一中", } fmt.Println("st1", st1) fmt.Println("st1 name", st1.hum.name) t1 := Teacher{} t1.school = "一中" t1.name = "lynn" t1.sex = "girl" t1.age = 20 fmt.Println("t1", t1) fmt.Println("t1 name", t1.name) // 继承的时候虽然我们没有声明变量名称,但是默认自动会给类型创建一个同名字段 // 这是为了能在子类中操作父类,因为:子类父类可能出现同名字段 fmt.Println("t1 age", t1.Human.age) } 类成员访问权限(字段大小写)...

5 min · 2016 words · Luenci

管道和go程

管道和go程 goroutine(go程) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package main import ( "fmt" "time" ) func display(num int) { count := 1 for { fmt.Println("============> 这是子go程:", num, "当前count值", count) count++ } } func main() { // 启动子go程 for i := 0; i < 3; i++ { go display(i) } // 主go程 count := 1 for { fmt.Println("============> 这是主go程:", count) count++ time.Sleep(1 * time.Second) } } 提前退出go程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package main import ( "fmt" "runtime" "time" ) // GOEXIT ===> 提前退出go程 // return ===> 返回当前函数 // exit ===> 退出当前进程 func main() { go func() { func() { fmt.Println("子go程内部的函数!") //return // 退出当前函数 //os.Exit(-1) // 退出进程 runtime.Goexit() // 退出当前go程 }() fmt.Println("子go程结束!") }() // 主go程需要等待子go程退出 fmt.Println("主go程~") time.Sleep(5 * time.Second) fmt.Println("OVER!") } 无缓冲管道 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package main import ( "fmt" "time" ) /* 当通道在多个协程之间传输的是指向数据的指针是,且读写操作是由不同的协程操作,则需要提供额外的同步动作。 */ func main() { // 当涉及到多go程时,c语言使用互斥量,上锁来保持资源同步,避免资源竞争问题 // go语言更好的解决方案是管道、通道 // 使用通道不需要手动进行加锁 //sync.RWMutex{} // 创建管道 关键字 chan numChan := make(chan int) // 装数字的管道,无缓冲通道,未声明空间 //numChan := make(chan int, 10) // 有缓冲通道 // 创建两个go程,父写,子读 // 发现子go程没有发生资源抢夺 // 子go程1 go func() { for i := 0; i < 25; i++ { // 只能 <- 数据流向 data := <-numChan fmt.Println("子go程1 读取data", data) } }() // 子go程2 go func() { for i := 0; i < 25; i++ { data := <-numChan fmt.Println("子go程2 读取data", data) } }() // 父go程 for i := 0; i < 50; i++ { // 向管道中写入数据 numChan <- i fmt.Println("====> 主go程,写入数据", i) } time.Sleep(5 * time.Second) } ...

5 min · 2255 words · Luenci

json、结构体标签和rpc入门

json、结构体标签和rpc入门 json使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package main import ( "encoding/json" "fmt" "reflect" ) // 将 结构体 --> 字符串 编码 // 将 字符串 --> 结构体 解码 // 结构体的成员须大写,不然不参与编码 type Student struct { Name string Sex string Age int Score int } func main() { st1 := Student{ Name: "luenci", Sex: "man", Age: 22, Score: 99, } // 编码 序列化 encodeInfo, err := json.Marshal(st1) if err != nil { fmt.Println("序列化发生错误,error", err) return } fmt.Println(reflect.TypeOf(encodeInfo)) fmt.Println(string(encodeInfo)) // 解码 反序列化 var st2 Student if err := json.Unmarshal([]byte(encodeInfo), &st2); err != nil { fmt.Println("反序列化发生错误,", err) return } fmt.Println(st2.Name) } ...

3 min · 1380 words · Luenci

golang常见错误之defer和迭代

defer 解析 作用域 向 defer 关键字传入的函数会在函数返回之前运行。假设我们在 for 循环中多次调用 defer 关键字: 1 2 3 4 5 6 7 8 9 10 11 12 func main() { for i := 0; i < 5; i++ { defer fmt.Println(i) } } $ go run main.go 4 3 2 1 0 运行上述代码会倒序执行传入 defer 关键字的所有表达式,因为最后一次调用 defer 时传入了 fmt.Println(4),所以这段代码会优先打印 4。我们可以通过下面这个简单例子强化对 defer 执行时机的理解: ...

4 min · 1704 words · Luenci

golang并发编程

预备知识 unsafe.Pointer unsafe.Pointer 是一种特殊意义的指针,它可以包含任意类型的地址,有点类似于 C 语言里的 void* 指针,全能型的。 对unsafe.Pointer 又爱又恨,你会有效使用它吗? unsafe 是关注 Go 程序操作类型安全的包。 unsafe.Pointer 可以让你无视 Go 的类型系统,完成任何类型与内建的 uintptr 类型之间的转化。根据文档,unsafe.Pointer 可以实现四种其他类型不能的操作: 任何类型的指针都可以转化为一个 unsafe.Pointer 一个 unsafe.Pointer 可以转化成任何类型的指针 一个 uintptr 可以转化成一个 unsafe.Pointer 一个 unsafe.Pointer 可以转化成一个 uintptr 两种只能借助 unsafe 包才能完成的操作: 使用 unsafe.Pointer 实现两种类型间转换 使用 unsafe.Pointer 处理系统调用。 CAS比较并交换—-Compare And Swap Go 的一个 CAS 操作使用场景 在并发执行的多个 routine R1,R2…Rn 的中,同一时间只允许唯一一个 routine 执行某一个操作,并且其他 routine 需要非阻塞的知道自己无权操作并返回的时候,可以使用 CAS 操作。 大方向:任务编排用 Channel,共享资源保护用传统并发原语 互斥锁实现机制 使用互斥锁,限定临界区只能同时由一个线程持有。 临界区 在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。 在 Go 标准库中,它提供了 Mutex 来实现互斥锁这个功能。 共享资源。并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要 Mutex、RWMutex 这样的并发原语来保护。 任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依赖的顺序关系,我们常常使用 WaitGroup 或者 Channel 来实现。 消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,常常使用 Channel 来实现。 ...

22 min · 10919 words · Luenci

golang中的nil

深入理解golang中的nil nil is (a) zero 什么是零值(zero value) 1 2 3 4 5 6 7 8 9 10 11 12 // go中的零值 bool -> false numbers -> 0 string -> "" pointers -> nil // point to nothing slices -> nil // have no backing array maps -> nil // are not initialized channels -> nil // are not initialized functions -> nil // are not initialized interfaces -> nil // have no value assigned, not even a nil pointer struct中的零值(zero values) 1 2 3 4 5 6 7 type Person struct{ Age int Name string Friend []Person } var p Person // Person{0,"",nil} nil 是什么类型(type) “nil is a predeclared identifier in Go that represents zero values for pointers, interfaces, channels, maps, slices and function types.” ...

2 min · 574 words · Luenci

Golang指针详解

Golang指针 原文来自:https://www.cnblogs.com/-wenli/p/12682477.html *类型:普通指针类型,用于传递对象地址,不能进行指针运算。 unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。 uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。 unsafe.Pointer 不能参与指针运算,比如你要在某个指针地址上加上一个偏移量,Pointer是不能做这个运算的,那么谁可以呢? 就是uintptr类型了,只要将Pointer类型转换成uintptr类型,做完加减法后,转换成Pointer,通过*操作,取值,修改值,随意。 小结 unsafe.Pointer 可以让你的变量在不同的普通指针类型转来转去,也就是表示为任意可寻址的指针类型。而 uintptr 常用于与 unsafe.Pointer 打配合,用于做指针运算。 unsafe.Pointer unsafe 是关注 Go 程序操作类型安全的包。 unsafe.Pointer 可以让你无视 Go 的类型系统,完成任何类型与内建的 uintptr 类型之间的转化。 unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述: (1)任何类型的指针都可以被转化为Pointer (2)Pointer可以被转化为任何类型的指针 (3)uintptr可以被转化为Pointer (4)Pointer可以被转化为uintptr unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在golang中是用于各种指针相互转换的桥梁,它可以包含任意类型变量的地址。 当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。 和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。 uintptr uintptr是一个整数类型。 1 2 3 // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr 即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收,这个需要注意! unsafe包 unsafe包只有两个类型,三个函数,但是功能很强大。 1 2 3 4 5 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中可以把Pointer类型,理解成任何指针的父类型。 三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。 unsafe.Sizeof 接受任意类型的值(表达式),返回其占用的字节数,这和c语言里面不同,c语言里面sizeof函数的参数是类型,而这里是一个表达式,比如一个变量。 unsafe.Offsetof:返回结构体中元素所在内存的偏移量。 Alignof 返回变量对齐字节数量Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数。 ...

8 min · 3728 words · Luenci

golang的组合和嵌套(面向接口编程)

官方关于golang的继承和重载的FAQ 原文部分来自:https://segmentfault.com/a/1190000022429780 关于类型继承 面向对象的编程,至少在最著名的语言中,涉及对类型之间关系的过多讨论,这些关系通常可以自动派生。Go 采取了不同的方法。 与其要求程序员提前声明两种类型是相关的,在 Go 中,类型会自动满足任何指定其方法子集的接口。除了减少簿记之外,这种方法还有真正的优势。类型可以同时满足多个接口,没有传统多重继承的复杂性。接口可以是非常轻量级的——具有一个甚至零个方法的接口可以表达一个有用的概念。如果出现新想法或用于测试,可以事后添加接口——无需注释原始类型。因为类型和接口之间没有明确的关系,所以没有要管理或讨论的类型层次结构。 可以使用这些想法来构建类似于类型安全的 Unix 管道的东西。例如,了解如何fmt.Fprintf 为任何输出启用格式化打印,而不仅仅是文件,或者bufio包如何与 文件 I/O 完全分离,或者image包如何生成压缩图像文件。所有这些想法都源于io.Writer表示单个方法 ( Write)的单个接口( )。而这只是皮毛。Go 的接口对程序的结构有着深远的影响。 这需要一些时间来适应,但这种隐式的类型依赖是 Go 最高效的事情之一。 ​ –faq: https://golang.org/doc/faq#inheritance 关于重载的定义 如果不需要进行类型匹配,则方法分派会得到简化。使用其他语言的经验告诉我们,拥有多种名称相同但签名不同的方法有时很有用,但在实践中也可能会令人困惑和脆弱。仅按名称匹配并要求类型的一致性是 Go 类型系统中一个主要的简化决定。 关于运算符重载,它似乎更方便而不是绝对要求。同样,没有它,事情会更简单。 ​ – faq:https://golang.org/doc/faq#overloading 从一个案例引入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 type ShapeInterface interface { Area() float64 GetName() string PrintArea() } type Shape struct { name string } func (s *Shape) GetName() string { return s.name } func (s *Shape) Area() float64 { return 0.0 } func (s *Shape) PrintArea() { fmt.Printf("%s : Area %v\r\n", s.GetName(), s.Area()) } // Rectangle 矩形求面积 type Rectangle struct { Shape w, h float64 } func (r *Rectangle) Area() float64 { return r.w * r.h } // Circle 圆形 : 重新定义 Area 和PrintArea 方法 type Circle struct { Shape r float64 } func (c *Circle) Area() float64 { return c.r * c.r * math.Pi } func (c *Circle) PrintArea() { fmt.Printf("%s : Area %v\r\n", c.GetName(), c.Area()) } func main() { s := Shape{name: "Shape"} c := Circle{Shape: Shape{name: "Circle"}, r: 10} r := Rectangle{Shape: Shape{name: "Rectangle"}, w: 5, h: 4} listshape := []ShapeInterface{&s, &c, &r} for _, si := range listshape { si.PrintArea() //!! 猜猜哪个Area()方法会被调用 !! } } out: Shape : Area 0 Circle : Area 314.1592653589793 Rectangle : Area 0 // 为啥这里没有调用 5 * 4 原因分析:Rectangle通过组合Shape获得的PrintArea()方法并没有去调用Rectangle实现的Area()方法,而是去调用了Shape的Area()方法。Circle是因为自己重写了PrintArea()所以在方法里调用到了自身的Area()。 ...

5 min · 2140 words · Luenci

Context 正确使用姿势(推荐)

Context 正确使用姿势 原文参考:https://juejin.cn/post/6844903929340231694 Context 是 immutable(不可变的) context.Context API 基本上是两类操作: 3个函数用于限定什么时候你的子节点退出; 1个函数用于设置请求范畴的变量 1 2 3 4 5 6 7 8 type Context interface { // 啥时候退出 Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error // 设置变量 Value(key interface{}) interface{} } 如何创建 Context? 在 RPC 开始的时候,使用 context.Background() 有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。 如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO() 如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子) Context 放哪? 把 Context 想象为一条河流流过你的程序 理想情况下,Context 存在于调用栈(Call Stack) 中 不要把 Context 存储到一个 struct 里 除非你使用的是像 http.Request 中的 request 结构体的方式 request 结构体应该以 Request 结束为生命终止 当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference) Request 结束,Context 就应该结束。 Context 包的注意事项 要养成关闭 Context 的习惯 特别是 超时的 Contexts 如果一个 context 被 GC 而不是 cancel 了,那一般是你做错了 1 2 ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2) defer cancel()、 使用 Timeout 会导致内部使用 time.AfterFunc,从而会导致 context 在计时器到时之前都不会被垃圾回收。 在建立之后,立即 defer cancel() 是一个好习惯。 ...

7 min · 3432 words · Luenci

聊聊 Wire 依赖注入

Wire 等依赖注入工具旨在简化初始化代码的管理 💡 依赖注入 是一种标准技术,用于生成灵活且松散耦合的代码,通过显式地为组件提供它们工作所需的所有依赖项。 Wire 介绍 参考文章:Golang依赖注入框架wire使用详解_魂影魔宅-CSDN博客_golang wire Wire 有两个基本概念:提供者(provider)和注入器(Injector)。 官方的定义: provider: a function that can produce a value. These functions are ordinary Go code. injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body. 提供者(provider):一个可以产生值的函数(通常是构造函数)。这些函数都是普通的 Go 代码 注入器(Injector):一个按依赖顺序调用提供者的函数。 使用 Wire,您编写注入器(Injector)的签名,然后 Wire 生成函数的主体。 ...

5 min · 2456 words · Luenci

defer的妙用之跟踪函数调用链

defer的妙用之跟踪函数调用链 本文参考:Tony Bai 老师的极客时间 《Tony Bai · Go语言第一课》 课程 使用 defer 可以跟踪函数的 执行过程 defer 会预计算参数(表达式进行求值): 详情见 https://luenci.com 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // trace.go package main func Trace(name string) func() { println("enter:", name) return func() { println("exit:", name) } } func foo() { defer Trace("foo")() bar() } func bar() { defer Trace("bar")() } func main() { defer Trace("main")() foo() } out: enter: main enter: foo enter: bar exit: bar exit: foo exit: main 程序的函数调用的全过程一目了然地展现在了我们面前:程序按 main -> foo -> bar的函数调用次序执行,代码在函数的入口与出口处分别输出了跟踪日志。 Go 会对 defer 后面的表达式Trace("foo")()进行求值。由于这个表达式包含一个函数调用Trace("foo"),所以这个函数会被执行。 程序存在的问题 调用 Trace 时需手动显式传入要跟踪的函数名; 如果是并发应用,不同 Goroutine 中函数链跟踪混在一起无法分辨; 输出的跟踪结果缺少层次感,调用关系不易识别; 对要跟踪的函数,需手动调用 Trace 函数 接下来我们一步一步的解决这些问题,来完善我们的函数调用链。 ...

6 min · 2516 words · Luenci