Go

golang 资源集

TT
pkg 集合 fdlr: 文件下载 Terminal progress bar for Go: 终端进度条 xwb1989/sqlparser: sql语句解析 sqlx: 官方database/sql 的扩展 cobra: CLI命令行 viper: 配置管理 博客&资源 GoLang导航

golang channle学习

TT
学习 golang channel 源码的记录,都注释在源码中了。 文章参考: 微信公众号-Golang梦工厂 极客时间专栏-Go并发编程实战课 channel 的数据结构 type hchan struct { qcount uint // 循环队列中元素的数量,就是还没被取走的 dataqsiz uint // 循环队列的大小 buf unsafe.Pointer // 存放元素的循环队列,大小总是elemsize的整数倍 elemsize uint16 // chan 中元素的大小 closed uint32 // chan 是否关闭 elemtype *_type // chan 中元素的类型 sendx uint // 处理发送数据的指针在 buf 中的位置。一旦接收了新的数据,指针就会加上 elemsize,移向下一个位置 recvx uint // 处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置 sendq waitq // 如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中 recvq waitq // chan 是多生产者多消费者的模式,如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中 lock mutex // 保护所有字段 } channel 的创建 使用 make 创建 channel 时,编译后对应 runtime.

Go sync.Map 学习

TT
一些结论 sync.Map 适用场景 只会增长的缓存系统中,一个 key 只写入一次而被读很多次; 多个 goroutine 为不相交的键集读、写和重写键值对。 官方叮嘱:建议你针对自己的场景做性能评测,如果确实能够显著提高性能,再使用 sync.Map。 sync.Map 源码中的一些思想 空间换时间。通过冗余的两个数据结构(只读的 read 字段、可写的 dirty),来减少加锁对性能的影响。对只读字段(read)的操作不需要加锁。 优先从 read 字段读取、更新、删除,因为对 read 字段的读取不需要锁。 动态调整。miss 次数多了之后,将 dirty 数据提升为 read,避免总是从 dirty 中加锁读取。 double-checking。加锁之后先还要再检查 read 字段,确定真的不存在才操作 dirty 字段。 延迟删除。删除一个键值只是打标记,只有在提升 dirty 字段为 read 字段的时候才清理删除的数据。 sync.Map 结构图 疑问 1. 为什么说 read 是并发读写安全的? 2. read 为什么可以更新 key 对应的 value?dirty 中会同步更新吗? 3. map 的 misses 是什么?干嘛用的? 4. 什么时候 misses 会变化? 5. readOnly 的 amended 是什么? 6. 什么时候会改变 amended? 7.

Go Cond 学习

TT
为等待 / 通知场景下的并发问题提供支持。Cond 通常应用于等待某个条件的一组 goroutine,等条件变为 true 的时候,其中一个 goroutine 或者所有的 goroutine 都会被唤醒执行。 说点人话吧…… cond 分析 我们来看一下 Cond 提供的方法 func NewCond(l Locker) *Cond {} // 创建一个 cond func (c *Cond) Wait() {} // 阻塞,等待唤醒 func (c *Cond) Signal() {} // 唤醒一个等待者 func (c *Cond) Broadcast() {} // 唤醒所有等待者 第一步:创建一个 cond c := sync.NewCond(&sync.Mutex{}) 第二步:将 goroutine 阻塞在 c 上 // 这里会有坑,下文再讨论 c.Wait() 第三步:唤醒 // 唤醒所有等待者 c.Broadcast() // 唤醒一个等待者 c.Signal() 这里再回过头去看 cond 的作用,应该清晰了不少,我们再结合一个例子来看一下。

Go sync.Once 学习

TT
Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资源,或者在测试的时候初始化一次测试资源。 如何初始化单例 1. 定义 package 级别的变量 package test var pi = 3.1415926 2. 在 init() 函数中初始化 package test var pi float64 func init() { pi = 3.1415926 } 3. 在 main 函数中执行初始化逻辑 package test var pi float64 func initApp() { pi = 3.1415926 } func main() { initApp() } 以上三种方法都是线程安全的,后两种方法甚至可以提供定制化的初始化服务,但是它们有一个共同的缺点:不能实现延迟初始化。 延迟初始化 // 使用互斥锁保证线程(goroutine)安全 var mu sync.Mutex var pi float64 func getPI() float64 { mu.Lock() defer mu.Unlock() if pi != 0 { return pi } pi = 3.

Go noCopy 实现禁止拷贝

TT
话不多说,直接上码: // noCopy may be embedded into structs which must not be copied // after the first use. // See https://golang.org/issues/8005#issuecomment-190753527 // for details. type noCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*noCopy) Lock() {} func (*noCopy) Unlock() {} 意思就是说,如果你想禁止一个 struct 被拷贝,只需把 noCopy 嵌入即可,go vet 命令会做禁止拷贝的检查。 package main func main() { var t = TestNoCopy{} Test(t) } type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {} type TestNoCopy struct { noCopy } func Test(t TestNoCopy) {} 将上述代码保存为 main.

Go 定时任务

TT
说起 Go 的定时任务,不得不学习一波 robfig/cron 包,github地址 1. 使用 Demo 1.1 每秒钟执行一次 package main import ( "fmt" "time" "github.com/robfig/cron/v3" ) func main() { job := cron.New( cron.WithSeconds(), // 添加秒级别支持,默认支持最小粒度为分钟 ) // 每秒钟执行一次 job.AddFunc("* * * * * *", func() { fmt.Printf("secondly: %v\n", time.Now()) }) job.Run() // 启动 } cron 表达式格式可以自行百度,这里不再赘述。 需要强调的是,cron 默认支持到分钟级别,如果需要支持到秒级别,在初始化 cron 时,记得 cron.WithSeconds() 参数。 1.2 每分钟执行一次 // 每分钟执行一次 job.AddFunc("0 * * * * *", func() { fmt.Printf("minutely: %v\n", time.Now()) }) 1.

Go 数据结构对齐

TT
理解的不深,把自己得到的结论以及参考的博文记录了下来,日常开发中注意 struct 字段的排序,记住这么多也足够了。 什么是数据结构对齐 维基百科:数据结构对齐是代码编译后在内存的布局与使用方式。包括三方面内容:数据对齐、数据结构填充(padding)与包入(packing)。 挺模糊的定义,我就简单理解为:数据在内存中的存储位置,不是那么单纯地一个挨一个,或是为了性能,或是为了安全,产生了一系列分布规则。 为什么要进行对齐 要解释为什么需要对齐,需要先说明一下 CPU 访问内存的方式:以字长(machine word)为单位访问,而非以字节(Byte)为单位访问。 也就是说 32 位 CPU 以 32 位(4字节)为单位访问内存,64 位 CPU 以 64 位(8字节)为单位访问内存。 知道了 CPU 访问内存的方式,如果不进行内存对齐,会增加 CPU 访问内存的次数,严重时会触发总线错误 32 位操作系统下,a、b 各占 3 字节,结合 CPU 的访问方式来看,在内存未对齐时,若访问 b 元素,需要进行两次内存访问。内存对齐后,a、b 各占 4 字节,此时访问 b 元素,只需要一次内存访问。 Go 内存对齐 对齐保证 可以通过 unsafe 包的 Alignof() 方法查看对齐保证,示例如下: var i8 int8 = 1 align := unsafe.Alignof(i8) var i32 int32 = 1 align = unsafe.Alignof(i32) type alignment guarantee bool, byte, uint8, int8 1 uint16, int16 2 uint32, int32 4 float32, complex64 4 arrays depend on element types structs depend on field types other types size of a native word 表格数据来源 概括来说就是:

Go 指针

TT
普通指针:*T 对于任意类型 T,它所对应的指针类型就是 *T var i int var ip *int var s string var sp *string Go 是强类型,不同类型对应的 *T 不可相互转换、不可相互赋值、不可做比较、普通指针不可参与计算。 万能指针:unsafe.Pointer unsafe.Pointer 与 *T 的关系,就好比 interface{} 和 T 的关系,也就是说 unsafe.Pointer 可以承载任意类型的 *T,它们之间可以互相转换,这就赋予了开发者直接操作指定内存的权力(效果不明显,配合 uintptr 服用效果最佳) var i int = 1 pointer := unsafe.Pointer(&i) // *int -> pointer p := (*int)(pointer) // pointer -> *int *p = 2 fmt.Println(i) // 2 unsafe.Pointer 提供的操作 // Pointer(*T) 将 *T 转化为 Pointer,也是结构体对应的内存的开始地址 type Pointer *ArbitraryType // Sizeof(T) 返回 T 占用字节数 func Sizeof(x ArbitraryType) uintptr // Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员 func Offsetof(x ArbitraryType) uintptr 魔幻指针:uintptr uintptr 解除了指针无法参与计算的封禁。官方对其定义为:

如何通过“功能选项”来设计友好的 API

TT
场景假设 你作为公司的顶梁柱,要编写一个服务器组件 阶段一:上手就撸版本 type Server struct { listener net.Listener } func (s *Server) Addr() net.Addr func (s *Server) Shutdown() func NewServer(addr string) (*Server, error) { l, err := net.Listen("tcp", addr) if err != nil { return nil, err } srv := Server{listener: l} go srv.run() return &srv, nil } 优点 三分钟即可上线,啥都不管,无脑 NewServer() 就完事了 缺点 一切皆为默认,想要定制是不可能的 阶段二:巨多参数版本 新的需求来了: 我要限制最大连接数 我要设置超时时间 我要设置 tls 证书 我要我还要…… 那就增加点参数,整个可配置版吧 // NewServer returns a new Server listening on addr.