Go 中 for range 和 slice 的坑

for range 的实现 下面这段代码会永无止境的循环吗 ? package main import ( "fmt" ) func main() { sl := []int{1,2,3,4} for _, v := range sl{ sl = append(sl, v) } fmt.Println(sl) } 要验证它很简单,运行一下即可得到结果,最后的结果是 [1 2 3 4 1 2 3 4] 要理解为什么会有这样的结果不难,首先我们需要清楚一点 go 语言中的赋值语句都是赋值,那么就意味着 如果赋值的是一个指针, 那么拷贝的是指针指向对象的地址(就是一个数值, 至于这个数值有多大, 具体要看运行的平台)也就是指针的值 如果赋值的是一个对象, 那么就会拷贝这个对象 然后,我们再来看一下,当 for range 遇到不同的迭代对象时,编译器是如何展开代码的 数组 range_temp := range len_temp := len(range) for index_temp = 0; index_temp < len_temp; index_temp++ { value_temp = range_temp[index_temp] index = index_temp value = value_temp original body } slice 切片 for_temp := range len_temp := len(for_temp) for index_temp = 0; index_temp < len_temp; index_temp++ { value_temp = for_temp[index_temp] index = index_temp value = value_temp original body } map // Lower a for range over a map. // The loop we generate: var hiter map_iteration_struct for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { index_temp = *hiter.key value_temp = *hiter.val index = index_temp value = value_temp original body } 注: ...

2022-07-29 · Me

记一次 go-redis 的 debug 过程

Background 本文背景是这样的: 有一个线上服务使用了 go-redis 库连接 redis,目前 QPS 也不是很高,大约每秒一次的样子,但是通过 log 发现每次 redis 操作花费的时间如下: redis call cost: 0 ms redis call cost: 2 ms redis call cost: 0 ms redis call cost: 1 ms redis call cost: 0 ms redis call cost: 17 ms redis call cost: 0 ms .... 正常一个简单的 redis get 操作耗费 0-3ms 时间可以理解,但是为什么会出现 17 ms 呢? 而且出现的频率还不低,大概每 30 个正常的中会出现一个。 尝试 debug 首先总结一下场景和条件 service 部署在 k8s 中,大概 10 个 pod 在运行。 整个 service 的 QPS 大概一秒一个,很低。 高延迟的情况大概每 30 个 log 出现一个。 service 使用简单的 redis get(),没有复杂操作。 但是 service 本身是有很多 go routine 并发的。 所以可能出现问题的地方 ...

2022-05-01 · Me

gRPC client 如何实现 TCP 重连

之前写过一篇 gRPC-go 建立 TCP 连接的过程 博客,主要研究了 client 程序启动后,如何与 server 建立 TCP 连接。 今天,在思考 redis-go 的连接池实现的时候,突然想到: 当 gRPC 的 TCP 连接断开后,能自动重连吗? 如果可以,是如何实现的 ? 首先要注意,这里指的是 TCP 连接,而不是 http2 中的 stream。 我们知道,gRPC 数据的传输使用 http2 的多路复用,也就是在一个 TCP 连接上有多个全双工的 http2 stream,这里的 stream 如果被断开后怎么重连与 http2 的实现有关,不在本文讨论范围。 对于上面第一个问题,使用 gRPC 的经验告诉我是可以自动重连的,不妨再做个简单的测试,client 端代码如下: func main() { conn, _ := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure()) defer conn.Close() cli := protobuf.NewTestClient(conn) req := &protobuf.EchoRequest{ Msg: "hi", } for i := 0; i < 10000; i++ { time.Sleep(time.Second) resp, err := cli.Echo(context.Background(), req) if err != nil { log.Printf("%v\n", err) continue } log.Printf("[D] resp: %s", resp.Reply) } } server 端代码略。 启动 client 后,不断启动和 ctrl-c 结束 server,证实 client 能自动重连 TCP 。 使用 netstat 查看 TCP 连接也能看到 client 使用了新的端口号重连。 ...

2022-04-04 · Me

Gin HTTP 框架学习笔记

最近要做一个 REST API server,在网上搜索了一遍以后,发现常用的是 Gin 和 Echo,并且很多人都说 golang 本身提供的 http server 已经足够强大,gin 和 echo 也只是在外包了一层。 我看 Gin 的源码行数比 Echo 少很多,而且测试覆盖率也高很多,因此决定学习一下 Gin,本文目标有以下这些 学习如何设计一个 REST 风格的 server ? 学习 Gin 在 go 自带的 http server 基础上做了哪些工作? 启动 Gin http server 在使用 Gin 框架的时候,最后都会调用 gin.Run(":8080") ,这样你的 http server 就可以就收 client 请求了, func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return } 可见,Run 函数最后调用了 http.ListenAndServe,所以说 http 协议层的解析等工作都是 go 标准库完成的,Gin 只负责后续针对不同 URL 的路由 (Router) 工作。 ...

2021-09-03 · Me

错误使用 time.After() 导致内存泄漏

今天看到了一篇有关 timer 泄露的文章,觉得很有意思,于是把它记录下来。 一般没有问题的写法 说道 time.After() 会导致内存泄露,很多人一定会觉得奇怪,因为代码里经常会用到它,也没见有内存泄漏啊? 是的,一般我们这样写的话是没有问题的 func main() { ch := make(chan int) go func() { ch <- 1 }() select { case _ = <-ch: case <-time.After(time.Second * 1): fmt.Println("timeout") } } 有问题的写法 那么,什么样的写法有问题呢? 当使用 for loop 的时候,比如这样 for { select { case _ = <-ch: // do something... continue case <-time.After(300 * time.Millisecond): fmt.Printf("time.After() fire!\n") } } 很不幸的是,上面这样的写法也非常常见,我自己就写过这样的代码。那么它真的会造成内存泄露吗?试一下便知道 前一篇博客中已经介绍了如何使用 pprof 对 Go 程序进行 profiling,简单提一下步骤 在代码中引入 _ "net/http/pprof", 并开启一个http server 导出 metrics 运行你的 binary 执行 go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap 浏览器就会自动打开 localhost:8081 显示结果了 测试代码如下: package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { go func() { ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }() ch := make(chan int, 10) go func() { in := 1 for { in++ ch <- in } }() for { select { case _ = <-ch: // do something... continue case <-time.After(3 * time.Minute): fmt.Printf("time.After() fire!\n") } } } 使用 pprof 后,可见 timer 内存占用居然超过了 1GB !! ...

2021-08-08 · Me

如何设计一个连接池

事情的起因是我在 k8s 中部署了一个 redis,然后 service A 使用 go-redis 库连接 redis。 这个时候我想到: service Pod 和 redis Pod 启动的顺序是不一定的,可能是 service Pod 先启动,此时 redis pod 还没有启动;又或者 redis pod 中途 restart 了。 go-redis 库能正确的处理重连吗? 简单的用 kubectl 命令删除、 重启了 redis,发现 service Pod 能自动恢复连接,说明 go-redis 正确进行了处理,那么它是怎么做的呢 ? 在寻找答案之前,先来想想如果是我自己实现,需要哪些功能? 该怎么实现? conn pool 需要自动删除已经断开的、坏掉的连接。 (开一个 goroutine 定期检查即可) 能自动新建连接,补齐一定数量的 conn。 (也不难,goroutine 即可) 如何检测一个 conn 是不是出错了? 对外的接口是 Get 和 Put,除了正常的用 mutext 控制并发以外,还有什么特殊的操作吗? go-redis 源码位于 redis/v8/internal/pool/pool.go , 首先看 pool.Options 数据结构 type Options struct { PoolSize int // 连接池数量 MinIdleConns int // 最小空闲连接数 MaxConnAge time.Duration // 连接最大生存时间 PoolTimeout time.Duration // IdleTimeout time.Duration // 空闲超时时间 IdleCheckFrequency time.Duration } 然后看 pool.ConnPool 结构体 ...

2021-05-01 · Me

Goroutine Pool 实现高并发

本文是读完 Handling 1 Million Requests per Minute with Go 之后,根据自己的理解,对文中提到的并发模型和实现再梳理一遍。 前言 假设有一个 http server 接收 client 发来的 request,如果用下面的这样的代码,会有什么问题呢? func payloadHandler(w http.ResponseWriter, r *http.Request) { // Go through each payload and queue items individually to be posted to S3 for _, payload := range content.Payloads { go payload.UploadToS3() // <----- DON'T DO THIS } } 显而易见,有 2 个问题: 接收一个 request 就开启一个 goroutine 处理,当 request 数量在短时间内暴增的话,光是 goroutine 的数量都足以让 server 崩溃。 每个 goroutine 都会与后端建立 TCP 连接,既耗费三次握手的时间,也会造成后端有大量 TCP 连接 所以,我们的目标是 没有蛀牙 可以控制 goroutine 的总数,方法是事先创建好一定数量的 goroutine,加入到一个 Pool 中 goroutine 启动时与后端建立 TCP 长连接,之后的通信都基于这个连接 根据原文作者给出的方法,整体的架构如下: ...

2020-10-13 · Me

gRPC-go 建立 TCP 连接的过程

首先看一个最简单的建立 client server 之间 gRPC 连接的代码,以这个代码为例,分析一下 TCP 是在何时建立的。 Server 端的代码相对来说很容易,一个最简单的 server 代码如下: func main() { lis, _ := net.Listen("tcp", fmt.Sprintf(":%d", 8080)) grpcServer := grpc.NewServer() protobuf.RegisterTestServer(grpcServer, &server{}) grpcServer.Serve(lis) } 在 grpc/server.go 中的 Serve() 函数调用了 lis.Accept() 并阻塞,当 client 端发来 TCP 请求时,Accept() 返回 Conn 结构,并开启 goroutine handleRawConn() 进行后续的处理。 就 TCP 来说,server 端的代码简单易懂,相比之下 client 端则不一样,一个基本的 Client 代码如下: func main() { conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure()) defer conn.Close() cli := protobuf.NewTestClient(conn) } 而要弄清楚 Client 端如何建立 TCP 却不容易,这是因为 grpc client 有 resolve DNS 以及做 load balancer 的功能,因此代码复杂很多。 从上面的代码不难看出肯定是在 Dial() 函数中建立的,它的具体实现是在 DialContext(), 返回 ClientConn 结构体指针,但是却看不到在哪建立了 TCP 链接,这是因为 TCP 链接是在一个 Goroutine 中异步建立的。如果想要 DialContext() 等连接建立完再返回,可以指定grpc.WithBlock()传入Options来实现。 ...

2020-10-11 · Me