小蚯蚓 - Golang
type: channel
link: https://xiaoqiuyinboke.cn/category/GO/
items: 空结构体的4大应用场景
items: Go代码实现阻塞的6种方案
items: req - Go语言HTTP客户端库 | 简单易用 & 强大功能 | imroc/req
items: Go开源包asynq：基于Redis的简单、可靠、高效的分布式任务队列
items: Golang本地缓存选型对比：BigCache、FreeCache、FastCache原理与性能分析
items: Sonic - 高性能JSON库
items: go 垃圾回收 gc
items: Golang性能分析 | 使用pprof进行性能优化
items: Go 语言接口语法指南 | 进阶接口语法学习
items: Go语言泛型指南| Go语言泛型教程

空结构体的4大应用场景
type: item
date: 2025-07-06T22:11:00+08:00
description: 在 Go 语言中，空结构体（struct{}） 是一个特殊的存在，它不占用任何内存（大小为 0 字节），但却有重要的实际用途。以下是它的 4大经典应用场景，结合代码示例详细说明： 1. 作为通道信号（无数据事件通知） 空结构体常用于 chan struct{} 实现纯信号通知，不传递数据，仅通过通道的开关或事件触发行为。 优势：零内存开销，语义明确（仅关注事件本身，不关心数据）。 func worker(done chan struct{}) { fmt.Println("working...") time.Sleep(1 * time.Second) done <- struct{}{} // 发送信号 } func main() { done := make(chan struct{}) go worker(done) <-done // 阻塞等待信号 fmt.Println("done!") } 2. 实现集合（Set 数据结构） Go 没有内置 Set 类型，通常用 map[T]struct{} 模拟集合。优势： 比 map[T]bool 更节省内存（struct{} 不占空间，bool 占 1 字节）。 语法清晰（用 _, exists := set[key] 判断存在性）。 func main() { set := make(map[string]struct{}) // 添加元素 set["apple"] = struct{}{} set["banana"] = struct{}{} // 检查元素是否存在 if _, ok := set["apple"]; ok { fmt.Println("apple exists") } } 3. 作为方法接收器（节省内存） 当方法不需要访问结构体字段时，可用空结构体作为接收器，避免为无状态操作分配额外内存。典型场景： 工具类方法（如单例模式）。 接口实现（仅需方法，不存储数据）。 type NullReceiver struct{} // 无状态方法 func (n NullReceiver) Print(msg string) { fmt.Println(msg) } func main() { var nr NullReceiver nr.Print("Hello, 空结构体接收器!") } 是的！使用空结构体作为 context.Context 的键（Key） 是 Go 中的另一个经典应用场景，尤其在需要高效、无内存占用的上下文值传递时。以下是详细说明和示例： 4. 作为 context.Context 的键（Context Key） 4.1. 1 为什么用空结构体？ 零内存开销：context.WithValue 的键通常是 interface{} 类型，但键本身的值会被存储。如果用空结构体作为键的类型（而非值），可以避免为键分配额外内存。 类型安全：通过自定义空结构体类型，避免键的字符串或基本类型冲突。 4.2. 示例代码 package main import ( "context" "fmt" ) // 定义自定义空结构体类型作为键的类型 type privateKey struct{} // 全局唯一的键（避免字符串键的命名冲突） var key = &privateKey{} func main() { ctx := context.Background() // 存储值到上下文（键是 key，值是 "secret"） ctx = context.WithValue(ctx, key, "secret") // 从上下文获取值 if val := ctx.Value(key); val != nil { fmt.Println("获取到的值:", val) // 输出: 获取到的值: secret } } 4.3. 关键点 避免键冲突：如果用字符串（如 ctx.WithValue(ctx, "my-key", value)），其他包可能意外覆盖你的键。而自定义空结构体类型是全局唯一的（指针地址唯一），天然防冲突。 极简内存：键 key 是一个指向空结构体的指针，但空结构体本身不占内存（privateKey{} 大小为 0），仅存储指针地址。 对比其他实现： 用 string 作为键：占用内存（字符串长度）。 用 int 作为键：可能与其他包的数字键冲突。 空结构体指针：最优解。 5. 其他类似场景 作为同步原语的标记：例如在 sync.WaitGroup 或 sync.Pool 中，空结构体可用于标记状态（尽管实际较少见）。 装饰器模式的无状态占位： 在需要实现接口但无需数据的装饰器中，空结构体可作为轻量级接收器。 6. 总结表：空结构体的四大应用 场景…

Go代码实现阻塞的6种方案
type: item
date: 2025-07-05T22:39:00+08:00
description: 1. 方式一：Sleep package main import "time" func main() { go func() {}() time.Sleep(time.Second) } 这种方式，主协程为了等待子协程结束，阻塞了 1s 时间。 好处是：主协程什么时候解除阻塞，不依赖子协程 缺点是：主协程需要等待多久不好确定 2. 方案二：WaitGroup func main() { wg := sync.WaitGroup{} wg.Add(1) go func() { // do something //如果忘记这个的话，主协程永远阻塞，runtime判断没事可错了，不如直接死锁panic wg.Done() }() wg.Wait() } 这种方式，主协程的解除阻塞条件依赖于子协程 3. 方案三：Channel func main() { // 零缓冲channel finish := make(chan struct{}) go func() { // do something //如果忘记这个的话，主协程永远阻塞，runtime判断没事可错了，不如直接死锁panic finish <- struct{}{} }() <-finish } 这种方式依赖于读取无缓冲通道会阻塞的特性，主协程依赖于子协程 4. 方案四：死循环 package main import "time" func main() { go func() { // do something }() for { time.Sleep(time.Second) } } 死循环里必须 sleep，不然的话比较消耗 cpu。for {} 是一个 忙等待（busy loop），会导致一个逻辑 CPU 核心持续满载（即使 goroutine 里没有实际工作） 5. 方案五：Select package main import "time" func main() { go func() { // do something // 如果没有一直执行的话，主协程永远阻塞，runtime判断没事可错了，不如直接死锁panic for { time.Sleep(time.Second) } }() select {} } 通过 select{} 来永久阻塞，但 不消耗 CPU，同时允许其他 goroutine 执行。 6. 方案六：锁 package main import ( "sync" "time" ) func main() { mutex := sync.Mutex{} // 第一次顺利抢到锁 mutex.Lock() go func() { // do something time.Sleep(5 * time.Second) //如果忘记这个的话，主协程永远阻塞，runtime判断没事可错了，不如直接死锁panic mutex.Unlock() // 锁释放 }() // 第二次拿不到锁，等待锁释放 mutex.Lock() }

req - Go语言HTTP客户端库 | 简单易用 & 强大功能 | imroc/req
type: item
date: 2025-03-24T22:16:38+08:00
description: 今天介绍一个可以跟fasthttp比肩的http库imroc/req 一、简介 imroc/req 是一个用于 Golang 的 HTTP 请求客户端库，其设计理念类似于 Python 的 Requests 库。该库提供了简单、直观的 API，使得开发者能够快速构造并发送 HTTP 请求，同时方便地处理响应数据。它适用于处理各种常见的 HTTP 请求场景，包括 GET、POST、PUT、DELETE 等，同时支持 JSON、表单数据、文件上传等多种内容类型。 二、特点 简单易用的 API 通过链式调用和简化的函数接口，极大地降低了发送 HTTP 请求的门槛，代码更加清晰易读。 丰富的数据处理能力 内置对 JSON 数据的自动编码与解码支持，能够快速将响应数据绑定到结构体或 Map 中，方便后续处理。 多种请求方式支持 支持 GET、POST、PUT、DELETE、PATCH 等常见的 HTTP 方法，可以轻松应对各类接口请求。 灵活的请求配置 可以通过设置 Header、Query 参数、Cookies 等自定义 HTTP 请求，同时支持超时、代理、重试等高级配置。 文件上传和多部分表单 内置对多部分表单数据（multipart/form-data）的支持，方便实现文件上传等需求。 错误处理与调试 对网络错误、超时等情况进行了良好的封装，同时支持调试模式，帮助开发者快速定位问题。 三、安装 使用 go get 命令即可安装该库： go get -u github.com/imroc/req 四、基本用法 1. GET 请求 下面示例展示了如何使用 req 发送一个 GET 请求，并将返回的 JSON 数据解析到一个 Map 中： package main import ( "fmt" "log" "github.com/imroc/req" ) func main() { // 发送 GET 请求，并带上自定义 Header resp, err := req.Get("https://httpbin.org/get", req.Header{ "User-Agent": "imroc-req", }) if err != nil { log.Fatalf("请求失败: %v", err) } // 将响应的 JSON 数据解析到 map 中 var result map[string]interface{} err = resp.ToJSON(&result) if err != nil { log.Fatalf("解析 JSON 失败: %v", err) } fmt.Println("GET 请求返回数据:", result) } 2. POST 请求 使用 POST 请求时，可以通过内置方法自动将数据编码为 JSON 或表单数据： package main import ( "fmt" "log" "github.com/imroc/req" ) func main() { // 构造 JSON 请求体 payload := req.BodyJSON(map[string]string{ "name": "imroc", "lang": "go", }) // 发送 POST 请求 resp, err := req.Post("https://httpbin.org/post", payload) if err != nil { log.Fatalf("请求失败: %v", err) } // 输出响应内容 fmt.Println("POST 请求返回:", resp) } 3. 文件上传 通过 multipart 表单上传文件同样十分简单： package main import ( "fmt" "log" "os" "github.com/imroc/req" ) func main() { // 打开需要上传的文件 file, err := os.Open("example.txt") if err != nil { log.Fatalf("打开文件失败: %v", err) } defer file.Close() // 构造 multipart/form-data 的请求体，其中 req.File 用于指定文件内容及文件名 payload := req.BodyFormMultipart(req.Param{ "file": req.File{ File: file, FileName: "example.txt", }, "description": "这是一个文件上传示例", }) // 发送 POST 请求 resp, err := req.Post("https://httpbin.org/post",…

Go开源包asynq：基于Redis的简单、可靠、高效的分布式任务队列
type: item
date: 2025-03-23T22:04:16+08:00
description: 今天给大家推荐一个基于 redis实现的简单、可靠且高效的分布式任务队列：asynq。该队列出自谷歌员工Ken Hibino。 项目的开源地址：https://github.com/hibiken/asynq ，星标star 6.1k，目前有29位贡献者。 asynq的架构设计 基于redis的集群，支持哨兵模式。因此具备了存储可横向扩展及高可用性。以下是asynq运行的顶层设计原理： 客户端将任务入队。 服务端从队列拉取任务，并对每个任务启动一个协程（称之为worker）进行处理。 队列中的任务能够被多个worker并发处理。 以下是该包的架构图： 主要特点 保证每个任务至少被消费一次。 支持定时任务 支持任务消费失败重试 支持延时队列功能 支持redis集群模式（可横向扩展）和Sentinals（高可用）模式 支持批量消费任务 自带完善的监控功能 接下来，我们通过基于async包来编写一个客户端程序和一个服务端程序，用于发送消息和消费消息。 安装包 通过go get命令安装包 go get -u github.com/hibiken/asynq 核心数据类型 首先，我们来看一下在使用该包时要用到的两个核心数据类型， redis连接配置类型：RedisClientOpt asynq包使用Redis作为消息代理，我们称之为 message broker。在和redis建立连接时，通过该类型来自定和redis建立连接的一些属性。比如redis的地址、密码等。 redisConnOpt := async.RedisClientOpt{ Addr: "localhost:6379", // Omit if no password is required Password: "mypassword", // Use a dedicated db number for asynq. // By default, Redis offers 16 databases (0..15) DB: 0, } 任务: Tasks 在 asynq中，一个工作单元被包装成一个 Task，在 Task类型中有两个字段：Type和 Payload。 // Type是一个字符串类型，代表该任务的类型. func (t *Task) Type() string // Payload 是消息体. func (t *Task) Payload() []byte 生产任务 往队列中发送消息包括3步：和redis建立连接、构建任务Task、发送Task到redis。 和redis建立连接 通过 NewClient函数和 redis建立连接 client := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"}) 支持redis集群 在和redis建立连接时，通过 asynq.RedisClusterClientOpt结构体配置集群。如下： client := asynq.NewClient(asynq.RedisClusterClientOpt{ Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, }) 同样，在消费侧和redis建立连接的时候也采用集群模式。 支持redis哨兵模式 为了支持高可用性，在出现故障时redis服务能够起到自动恢复的目的，asynq包还支持redis的哨兵模式。如下： var redis = &asynq.RedisFailoverClientOpt{ MasterName: "mymaster", SentinelAddrs: []string{"localhost:5000", "localhost:5001", "localhost:5002"}, } client := asynq.NewClient(redis) 构建任务 通过NewTask构建任务。但在构建任务之前，需要先根据自己的业务定义消息体的结构。这里我们定义了一个 EmailTaskPayload的消息体结构，被消费时用于业务处理的数据。 type EmailTaskPayload struct { // ID for the email recipient. UserID int } payload, err := json.Marshal(EmailTaskPayload{UserID: 42}) //构建了2个任务，一个任务类型为"email:welcome" t1 := asynq.NewTask("email:welcome", payload) //构建任务，任务类型是 "email:reminder…

Golang本地缓存选型对比：BigCache、FreeCache、FastCache原理与性能分析
type: item
date: 2025-03-23T21:57:00+08:00
description: 提到本地缓存大家都不陌生，只要是个有点经验的后台开发人员，都知道缓存的作用和弊端。本篇文章我们就来简单聊聊在golang做业务开发的过程中，本地缓存的一些可选的开源方案。分析它们的特点，以及内部的实现原理。 1.本地缓存需求分析 首先来梳理一下业务开发过程中经常面临的本地缓存的一些需求。我们一般做缓存就是为了能提高系统的读写性能，缓存的命中率越高，也就意味着缓存的效果越好。其次本地缓存一般都受限于本地内存的大小，所有全量的数据一般存不下。那基于这样的场景一方面是想缓存的数据越多，则命中率理论上也会随着缓存数据的增多而提高；另外一方面是想，既然所有的数据存不下那就想办法利用有限的内存存储有限的数据。这些有限的数据需要是经常访问的，同时有一定时效性(不会频繁改变)的。基于这两个点展开，我们一般对本地缓存会要求其满 足支持过期时间、支持淘汰策略。最后再使用自动管理内存的语言例如golang等开发时，还需要考虑在加入本地缓存后引发的GC问题。 分析完我们日常本地缓存的诉求，再结合我们日常开发用到的golang语言，我们可以提炼得到golang本地缓存组件必须具备以下几个能力： 分析清楚了我们的需求，也明确了我们需要的能力。那自然优先考虑golang内置的标准库中是否存在这样的组件可以直接使用呢？ 很遗憾，没有。golang中内置的可以直接用来做本地缓存的无非就是map和sync.Map。而这两者中，map是非并发安全的数据结构，在使用时需要加锁；而sync.Map虽然是线程安全的。但是需要在并发读写时加锁。此外二者均无法支持数据的过期和淘汰，同时在存储大量数据时，又会产生比较频繁的GC问题，更严重的情况下导致线上服务无法稳定运行。 既然标准库中没有我们满足上述需求的本地缓存组件，那我们就想只有两种解决方案了 业界是否有开源成熟的方案可供选择 业界无可用组件时，自己动手写一个 那首先面临的第一个问题就是方案的调研和选型，没有合适的方案时自己再来动手构建。下面我们就来给大家介绍下golang中哪些可以直接来使用的本地缓存组件吧。 2.golang本地缓存组件概览 golang中本地缓存方案可选的有如下一些： 1. freecache 2. bigcache 3. fastcache 4. offheap 5. groupcache 6. ristretto 下面通过笔者一段时间的调研和研究，将golang可选的开源本地缓存组件汇总为下表，方便大家在方案选型时作参考。 在上述方案中，freecache、bigcache、fastcache、ristretto、groupcache这几个大家根据实际的业务场景首选，offheap有定制需求时可考虑。 通过上表的总结，个人想在此再谈几点关于本地缓存组件的理解： 1.上述本地缓存组件中，实现零GC的方案主要就两种： a.无GC：分配堆外内存(Mmap) b.避免GC：map非指针优化(map[uint64]uint32)或者采用slice实现一套无指针的map c.避免GC：数据存入[]byte slice(可考虑底层采用环形队列封装循环使用空间) 2.实现高性能的关键在于： a.数据分片(降低锁的粒度) 上述几种缓存组件每种组件的实现都是1和2的几个分支的组合。下面我们大概给大家介绍每种组件的核心原理。 3. 主流缓存组件实现原理剖析 在本节中我们会重点分析下freecache、bigcache、fastcache、offheap这几个组件内部的实现原理。 3.1 freecache实现原理 首先分析下freecache的内部实现原理。在freecache中它通过segment来进行对数据分片，freecache内部包含256个segment，每个segment维护一把互斥锁，每一条kv数据进来后首先会根据k进行计算其hash值，然后根据hash值决定当前的这条数据落入到哪个segment中。 对于每个segment而言，它由索引、数据两部分构成。 索引：其中索引最简单的方式采用map来维护，例如map[uint64]uint32这种。而freecache并没有采用这种做法，而是通过采用slice来底层实现一套无指针的map，以此避免GC扫描。 数据：数据采用环形缓冲区来循环使用，底层采用[]byte进行封装实现。数据写入环形缓冲区后，记录写入的位置index作为索引，读取时首先读取数据header信息，然后再读取kv数据。 在freecache中数据的传递过程是：freecache->segment->(slot,ringbuffer) 下图是freecache的内部实现框架图。 总结： freecache通过利用数据分片减小锁的粒度，然后再存储时索引并没有采用内置的map来维护而是采用自建m…

Sonic - 高性能JSON库
type: item
date: 2024-03-23T10:52:54+08:00
description: 介绍 我们在日常开发中，常常会对JSON进行序列化和反序列化。Golang提供了encoding/json包对JSON进行Marshal/Unmarshal操作。但是在大规模数据场景下，该包的性能和开销确实会有点不够看。在生产环境下，JSON 序列化和反序列化会被频繁的使用到。在测试中，CPU使用率接近 10%，其中极端情况下超过 40%。因此，JSON 库的性能是提高机器利用率的关键问题。 Sonic是一款由字节跳动开发的一个全新的高性能、适用广泛的 JSON 库。在设计上借鉴了多款JSON库，同时为了实现对标准库的真正插拔式替换，Sonic使用了 JIT** （即时编译）**。 Sonic的特色 我们可以看出：Sonic是一个主打快的JSON库。 运行时对象绑定，无需代码生成 完备的 JSON 操作 API 快，更快，还要更快！ Sonic的设计 针对编解码动态汇编的函数调用开销，使用 JIT 技术在运行时组装与模式对应的字节码（汇编指令），最终将其以 Golang 函数的形式缓存在堆外内存上。 针对大数据和小数据共存的实际场景，使用预处理判断（字符串大小、浮点数精度等）将 SIMD 与标量指令相结合，从而实现对实际情况的最佳适应。 对于 Golang 语言编译优化的不足，使用 C/Clang 编写和编译核心计算函数，并且开发了一套 asm2asm 工具，将经过充分优化的 x86 汇编代码转换为 Plan9 格式，最终加载到 Golang 运行时中。 考虑到解析和跳过解析之间的速度差异很大， 惰性加载机制当然也在 AST 解析器中使用了，但以一种更具适应性和高效性的方式来降低多键查询的开销。 在细节上，Sonic进行了一些进一步的优化： 由于 Golang 中的原生汇编函数不能被内联，发现其成本甚至超过了 C 编译器的优化所带来的改善。所以在 JIT 中重新实现了一组轻量级的函数调用： 全局函数表+静态偏移量，用于调用指令 使用寄存器传递参数 Sync.Map 一开始被用来缓存编解码器，但是对于准静态（读远多于写），元素较少（通常不足几十个）的场景，它的性能并不理想，所以使用开放寻址哈希和 RCU 技术重新实现了一个高性能且并发安全的缓存。 详细设计文档可查阅introduction 安装使用 当前我使用的go version是1.21。 官方建议的版本: Go 1.16~1.21 Linux / MacOS / Windows（需要 Go1.17 以上） Amd64 架构 # 下载sonic依赖 $ go get github.com/bytedance/sonic 基本使用 sonic提供了许多功能。本文仅列举其中较为有特色的功能。感兴趣的同学可以去看一下官方的examples 序列化/反序列化 sonic的使用类似于标准包encoding/json包的使用. func base() { m := map[string]interface{}{ "name": "z3", "age": 20, } // sonic序列化 byt, err := sonic.Marshal(&m) if err != nil { log.Println(err) } fmt.Printf("json: %+v\n", string(byt)) // sonic反序列化 um := make(map[string]interface{}) err = sonic.Unmarshal(byt, &um) if err != nil { log.Println(err) } fmt.Printf("unjson: %+v\n", um) } // print // json: {"name":"z3","age":20} // unjson: map[age:20 name:z3] sonic还支持流式的输入输出 Sonic 支持解码 io.Reader 中输入的 json，或将对象编码为 json 后输出至 io.Writer，以处理多个值并减少内存消耗 func base() { m := map[string]interface{}{ "name": "z3", "age": 20, } // 流式io编解码 // 编码 var encbuf bytes.Buffer enc := sonic.ConfigDefault.NewEncoder(&encbuf) if err := enc.Encode(m); err != nil { log.Fatal(err) } else { fmt.Printf("cutomize encoder: %+v", encbuf.String()) } // 解码 var decbuf bytes.…

go 垃圾回收 gc
type: item
date: 2024-03-04T18:07:02+08:00
description: 概述 垃圾回收（Garbage Collection），简称 GC，对于高级语言是有必需的，因为高级语言要更加关注的是业务，不能花太多精力去关注底层内存释放的问题。 v 1.3 的标记清除算法 标记清除算法，可以概括为以下 4 步骤，循环重复，直到 process 程序生命周期结束： 暂停程序（不然期间会产生新的对象），从 main 根函数出发，找到可达对象和不可达对象。 然后将可达对象变标记上。 然后，清除未标记的对象。 恢复程序运行。 缺点： stw（stop the world）让程序暂停，会表现出卡顿现象，这个是最重要的问题 标记步骤，需要扫描整个 heap（堆），耗费时间多 清除数据，会产生 heap 碎片 在 go V1.3 之前使用的是这个算法。不过 go 做了点优化，为了缩短了 stw 的时间范围，将第三步和第四步骤做了替换。mark 标记完成后，就恢复程序的运行，清除工作和业务程序并行运行。 v 1.5 的三色标记算法 三色标记法有白、灰、黑三个颜色的列表。 白色的列表代表没有被遍历到的， 灰色代表正在被遍历的临时状态， 黑色代表是遍历过的。 经过遍历后，最终只会剩下白色和黑色的两种，把白色的不可达对象给清除掉即可。 工作过程如下所示： 第一步，只要是新创建的对象，默认的颜色都是标记为"白色"。所以，以下的这些对象都会在白色列表中。 第二步，每次 GC 回收开始，首先遍历根节点，非递归，只遍历一次。把遍历到的对象从白色集合放入"灰色"集合。 如下图中，根结点遍历会操作，会遍历到对象 1 和对象 4。然后把着两个对象放到灰色标记表里。 第三步，开始遍历灰色集合，将灰色对象引用的对象从白色集合放入灰色集合中，之后将已经遍历完成的灰色对象放入黑色集合。 如上图所示，遍历灰色 对象 1 和灰色 对象 4，得到他们的引用 对象 2 和 对象 7，然后把 对象 2 和 对象 7 放到灰色列表里，对象 1 和对象 4 放到黑色列表里。 第四步，重复第三步，直到灰色中无任何对象。遍历灰色列表里的对象 2 和对象 7，把对象 2 的引用对象 3 变成灰色，然后对象 2 和对象 7 已经遍历完就变成黑色。 重复几次后，最终会得到如下所示的表 第五步：回收所有的白色标记表的对象 5 和对象 6. 也就是回收垃圾。 和普通标记清除算法比有什么不同点 三色标记法是一层层的开始标记，不是一次性全部标记 三色标记法，没有 stw，什么情况下会出现对象丢失？ 如果此时 gc 执行到下图这个环节的时候，因为没有 stw，程序还在运行，如果发生以下两个动作 白色对象被黑色对象 4引用 灰色 对象 2 丢失了和该 白色对象3 的关系 此时因为灰色 对象 2 已经没有引用 对象 3 了，所以遍历 对象 2 的时候，没法找到 对象 3，对象 3 就会被当做垃圾误清除 强弱三色不变式，确保对象不丢失 强三色不变式：允许黑色引用灰色（已经遍历了灰色，不会丢失灰色），不允许黑色引用白色（白色如果是上述的这个两个条件就会丢失，所以直接不允许），这样就会破坏上述中的条件 1 弱三色不变式：允许黑色引用白色，但是，该白色的上游链中必须有一个灰色引用到，这样该灰色最终会遍历到这个白色。 屏障机制，确保不被误删除 插入屏障： 对象被引用的时候触发的机制，在 a 对象引用 b 对象的时候，b 对象被标记成灰色，满足了强三色不变式，这样就不存在黑色引用白色的了。 不足点是：因为这个插入屏障只有在写的堆上才有，栈（用于函数调用啥的）上的空间比较少，要求效率要高，出于效率的考虑，栈上没有这个机制。所以三色标记结束的时候，要 stw 来重新扫描栈，大约 10-100ms。 删除屏障： 被删除的对象，如果自身是灰色或者白色，那么被标记成灰色，满足弱三色不变公式。如下图中，对象 5 和对象 1 之间的引用关系被删除的话，这个时候对象 5 设置成灰色就好。 但是这操作的话有个问题，这个对象 5 其实已经是垃圾了，却还存在，只能等到下一轮 gc 的时候，才会被清除。虽然会有删除精度不高的问题，但是能确保不会被错删除 v 1.8 三色标记法+混合写屏障 混合写屏障，就是把删除和插入屏障放在一起，加上一个栈上的默认全部黑操作，整个过程不会有stw，具体操作： GC 开始将栈上的对象可达结点全部扫描并标记为黑色 (之后不再进行第二次重复扫描，无需 STW) GC 期间，任何在栈上创建的新对象，均为黑色，下一轮 gc 删除 堆上被删除的对象标记为灰色 堆上被添加的对象标记为灰色

Golang性能分析 | 使用pprof进行性能优化
type: item
date: 2024-02-25T21:57:00+08:00
description: 什么是 pprof pprof 是 Go 官方提供的性能分析工具，可以分析程序的运行情况，并且提供可视化的功能。prof 是 profile（画像）的缩写，使用 pprof 可以分析以下几种指标数据： allocs：程序启动之后内存分配的情况 block：导致阻塞操作的一些堆栈跟踪信息 cmdline：当前程序启动的命令行 goroutine：所有当前 goroutine 的堆栈跟踪信息 heap：程序在当前堆上内存分配的情况 mutex：锁资源的竞争的堆栈信息 profile：CPU profile 文件。可以在 debug/pprof?seconds=x秒 GET 参数中指定持续时间。获取 pprof 文件后，使用 go tool pprof x.prof 命令分析 pprof 文件。 threadcreate：系统线程的使用情况 trace：当前系统的代码执行的链路情况 使用 pprof 工具主要分析以下几种指标： CPU Profiling：CPU 分析，按照一定的频率采集所监听的应用程序 CPU（含寄存器）的使用情况，可确定应用程序在主动消耗 CPU 周期时花费时间的位置 Memory Profiling：内存分析，在应用程序进行堆分配时记录堆栈跟踪，用于监视当前和历史内存使用情况，以及检查内存泄漏 Block Profiling：阻塞分析，记录 goroutine 阻塞等待同步（包括定时器通道）的位置 Mutex Profiling：互斥锁分析，报告互斥锁的竞争情况。 当程序存在内存或者 CPU 飙升的情况时，我们可以通过 pprof 工具来查询问题出现的根源。 怎么使用 pprof pprof 包含两个相关的库： runtime/pprof 主要应用于工具型应用。包含脚本、定时任务等。 如：对于只跑一次的程序，例如每天只跑一次的离线预处理程序，调用 pprof 包提供的函数，手动开启性能数据采集 net/http/pprof 主要应用于服务型应用。包含 HTTP 服务，GRPC 服务等。 如：对于在线服务，对于一个 HTTP Server，访问 pprof 提供的 HTTP 接口，获得性能数据。当然，实际上这里底层也是调用的 runtime/pprof 提供的函数，封装成接口对外提供网络访问。 工具型应用 工具型应用主要使用 runtime/pprof 包实现性能分析。 func main() { // --- cpu 分析示例 start--- // 创建cpu分析文件 fc, err := os.Create("./cpu.prof") if err != nil { fmt.Println("create cpu.prof err:", err.Error()) return } defer fc.Close() // 开始分析cpu err = pprof.StartCPUProfile(fc) if err == nil { defer pprof.StopCPUProfile() } // --- cpu 分析示例 end--- var count int for i := 0; i < 10000; i++ { count++ } // --- 内存 分析示例 start--- fm, err := os.Create("./memory.prof") if err != nil { fmt.Println("create memory.prof err:", err.Error()) return } defer fm.Close() // 开始分析内存 err = pprof.WriteHeapProfile(fm) if err != nil { fmt.Println("write heap prof err:", err.Error()) return } // --- 内存 分析示例 end--- for i := 0; i < 10000; i++ { count++ } fmt.Println("do finish......count:", count) } 执行 go run main.go 后，在代码目录下，可以看到生成了 cpu.prof 和 memory.prof 文件。 通过执行 go tool pprof ./memory.prof 或者 go tool pprof -http=:8888 ./memory.prof 进入命令行模式或者 web 页面进行性能分析。 执行 go tool pprof ./memory.prof 进入命令行： 执行 go tool -http=:8888 pprof ./memory.prof 可进入 web 页面，更方便查看： 页面展示效…

Go 语言接口语法指南 | 进阶接口语法学习
type: item
date: 2024-01-03T21:43:00+08:00
description: 概述 接口主要是用于实现多态的效果，接口是一个类型，一个抽象的类型，既然是类型，就可以定义接口类型的变量。 定义 接口好像用于定义方法的，写上方法签名列表即可 type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 // 签名 方法名2( 参数列表2 ) 返回值列表2 … } 接口名称：我们一般会在接口名称后面添加 er。 方法签名： 当方法名首字母是大写且这个接口类型名首字母也是大写时，这个方法可以被接口所在的包（package）之外的代码访问，也就是说可以被其它包所实现； 参数列表和返回值列表中的参数 变量名可以省略 type sayer interface { Say() string } 结构体变量赋值 如果一个变量定义为接口类型的变量，那么如果一个结构体实现了这个接口，并且方法的接收者都是值类型的，那么这个结构体的值类型和指针类型都可以赋予给这个接口变量。 例如下例中，结构体 dog，实现了 Mover 接口的 move 和 eat 方法，并且这个两个方法的接受者都是值类型，所以 dog{} 可以赋值给这个 Mover 接口变量，&dog{} 也可以赋值给这个 Mover 接口变量 package main import "fmt" type Mover interface { move() eat() } // dog 类型的方法的接受者都是值类型 type dog struct{} func (d dog) move() { fmt.Println("狗会动") } func (d dog) eat() { fmt.Println("吃") } func main() { var x Mover x = &dog{} // 指针类型可以赋值 x.move() var y Mover y = dog{} // 值类型可以赋值 y.move() } 但是，如果结构体方法的接收者存在指针类型，那么只能这个结构体的指针类型都可以赋予给这个接口变量，值类型不行，会报错。 如下所示 move 方法是指针类型，所以只能 &dog{} 赋予给 Mover 接口变量，用 dog{} 值类型赋予接口变量的话，将会报错 package main import "fmt" type Mover interface { move() eat() } type dog struct{} // move 方法的接受者是指针类型*dog func (d *dog) move() { fmt.Println("狗会动") } func (d dog) eat() { fmt.Println("吃") } func main() { var x Mover x = &dog{} // 指针类型可以赋值 x.move() var y Mover y = dog{} // 值类型不可以赋值了，会报错!!! y.move() } 空接口 接口是行为规范的集合，空接口没有任务行为规范，也就是接口方法为空。因为没有任何行为规范，所以任何类型都可以属于空接口类型。所以空接口可以接收任何参数。 package main import "fmt" // Mover 自定义空接口 type Mover interface { } // dog 空接口参数arg func dog(arg Mover) { } func main() { // 空接口可以接受任何参数 dog(10) dog("10") // 空接口可以接受任何参数 var a Mover a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } go 里其实已经自带了一个空接口类型 interface{}，不需要我们自定义（上例的 Mover 空接口） package main import "fmt" func main() { // go里自带的空接口类型interface{} var a interface{} a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } 在 go1.18 之后，官方更加建议使用 any 来替代 interface{}，这样可以少写几个字母，没有别的差别。 package main import "fmt" func main() { // go1.18之后，官方推荐用any替代interface{} var a any a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } 断言 由于空接口可以存任何类型的值，有时候我们想要知道，空接口类型的值到底是什么类型，这个时候就需要对值进行断言…

Go语言泛型指南| Go语言泛型教程
type: item
date: 2023-12-31T18:44:00+08:00
description: 从语法的角度来讲，泛型是从接口层面演化而来的。 下面例子中，f1 是依赖于接口类型 Man，f2 是依赖于 T 类型，T 类型等价于 Man 类型。 package main type Man interface { Say() } type Student struct { } func (s Student) Say() { } // 面向接口编程，a必须是Man接口类型 func f1(a Man) { } // 泛型，中括号里可以理解为T是Man的别名 // 既然T等Man，所以f1和f2就等价了 func f2[T Man](a T) { } func main() { f1(Student{}) f2(Student{}) } 应用在函数里 跟接口的语法有点类似，就是里面的内容是数据类型，而不是方法签名，如下所示 Happy 就是联合类型，将来要使用到 Happy 的时候，只需要用到任意一个类型传递进去就可以。 // 联合类型 type Happy interface { // ~T，是go1.18新增的符号，~t表示底层是T的所有类型 int | ~int32 | int8 | string | ~bool } // a,b必须同时为int、~int32、int8、string、 ~bool // 不可以一个是int，一个是string func f3[T Happy](a, b T, c float64) { } func main() { // a,b都是int f3(1, 2, 3.1) f3(true, false, 3.1) f3("a", "b", 3.1) } 如果不想要定义个结构体，可以如下所示，当然，a, b 两个参数每次调用的时候，也要是同一个类型。 package main func f4[T int | string | bool](a, b T, c float64) { } func main() { // a,b都是int f4(1, 1, 3.1) f4(true, false, 3.1) f4("a", "b", 3.1) } 应用在结构体 在接口体名称后面新增一个中括号，在中括号里设置好别名 package main import "fmt" type Happy interface { // ~T，是go1.18新增的符号，~t表示底层是T的所有类型 int | ~int32 | int8 | string | ~bool } type Bird[T Happy, S int | string] struct { Head T Hair S } func (b *Bird[T, S]) SetHead(h T) { b.Head = h } func main() { // [int, string]，定下来[T, S]是什么 bird := Bird[int, string]{ Head: 3, Hair: "lang", } fmt.Println(bird.Head, bird.Head) }