- 入门教程
- 最佳实践
- 错误案例
- 疑难答疑
- golang中如何表示枚举类型?">golang中如何表示枚举类型?
- defer和return谁先执行?多个defer是什么顺序?">defer和return谁先执行?多个defer是什么顺序?
- 具名返回值、匿名返回值对defer有什么影响?">具名返回值、匿名返回值对defer有什么影响?
- golang的struct里面嵌入interface?">golang的struct里面嵌入interface?
- golang 匿名结构体成员,具名结构体成员,继承,组合">golang 匿名结构体成员,具名结构体成员,继承,组合
- nil 是什么?">nil 是什么?
- Go Channel 详解">Go Channel 详解
- 序列化和反序列化:omitempty 总结">序列化和反序列化:omitempty 总结
- 字段参数验证库">字段参数验证库
入门教程
唯一推荐教程:Go语言入门指南,这个指南一共分为以下几个部分:
最佳实践
这个部分用于记录在看项目代码以及学习的过程中所见过的优秀案例
并发模式
在实际开发中,实际主要有以下几种并发模式:
- 资源冲突:利用锁机制(sync.Lock和sync.Unlock)进行资源隔离,例如多个协程都需要往同一个切片中写入数据;
- 等待子线程退出:主线程需要等待子线程(即协程)退出完毕之后才能退出或往下继续运行:
- 单个子线程:使用无阻塞通道
- 多个子线程:使用sync.WaitGroup等待批量子线程退出
- “赢者通吃”:多个子线程仅需要等待一个子线程完成就退出;
- 安全退出:如何安全、快速地停止多个子线程运行呢?
下面将一一给出示例,如有错漏和可以改进的地方,希望不吝赐教!
资源冲突
锁机制在Go中是一种比较“重”的并发模式,不是迫不得已不建议使用,例如某些业务场景下需要将多个子线程的数据写入同一个切片中:
allArr := make([]int, 0) var lock sync.Mutex for i := 0; i < 10; i++ { go func() { defer lock.Unlock() lock.Lock() allArr = append(allArr, i) }() }
通过加锁的方式避免了同时对同一切片进行写入所造成的冲突,但其实上述方式有一种更优雅的做法:将子线程的结果放入通道中,单独起一个子线程负责读取通道中的数据并写入最终的数组内。
等待子线程退出
根据Go语言内存模型规范,channel有三个特征:
- 向有缓存通道写入一个数据总是 happen before这个数据被从通道中读取完成;
- 从无缓存通道读取数据 happen before 向通道写入数据完成;
- 从容量为 C 的通道读取第 K 个元素 happen before 向通道第 k+C 次写入完成,比如从容量为 1 的通道接受第 3 个元素 happen before 向通道第 3+1 次写入完成。
单个子线程:
func main() { done := make(chan int, 1) go func() { fmt.Println(“hello, world”) done <- 1 }() <-done }
多个子线程:虽然可以通过扩大通道缓存同时等待多个子线程,但是有一个更简单的做法,那就是使用sync.WaitGroup。
func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println(“hello, world”) }() } wg.Wait() // 等待所有子线程退出 }
可以理解为,wg.Add和wg.Done分别进行+1和-1操作,而wg.Wait会阻塞直到该值等于0。
赢者通吃
赢者通吃意味着多个子线程只需要获取一个子线程的结果,《Go语言高级编程》有个例子:利用多个搜索引擎搜索同一个关键词,谁先返回就使用谁的结果:
func main() { ch := make(chan string, 32) go func() { ch <- searchByBing(“golang”) }() go func() { ch <- searchByGoogle(“golang”) }() go func() { ch <- searchByBaidu(“golang”) }() fmt.Println(<-ch) }
安全退出
在某些情况下,当程序运行过程中已经出现错误时,需要停止子线程的工作,例如循环、资源访问等等。这时可以利用select+channel或是使用context。
select+channel
func worker(cannel chan bool) { for { / 其他操作….. / select { case <-cannel: // 子线程退出 default: fmt.Println(“continue……”) // 正常工作 } } } func main() { cannel := make(chan bool) go worker(cannel) time.Sleep(time.Second) cannel <- true }
如果需要关闭多个子线程,可以直接close通道,从而向所有子线程进行广播退出的指令,并结合之前提到的sync.WaitGroup实现安全退出:
func worker(wg sync.WaitGroup, cannel chan bool) { defer wg.Done() for { select { default: fmt.Println(“continue……”) case <-cannel: return } } } func main() { cancel := make(chan bool) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go worker(&wg, cancel) } time.Sleep(time.Second) close(cancel) wg.Wait() }
context
Go 1.7新增了一个context包,用以简化单个请求在多个协程之间的超时、退出等操作。
func worker(ctx context.Context, wg sync.WaitGroup) error { defer wg.Done() for { select { default: fmt.Println(“continue…..”) case <-ctx.Done(): return ctx.Err() } } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go worker(ctx, &wg) } time.Sleep(time.Second) cancel() wg.Wait() }
Recover防止程序抛出panic而中止
func protect(g func()) { defer func() { log.Println(“done”) // Println executes normally even if there is a panic if err := recover(); err != nil { log.Printf(“run time panic: %v”, err) } }() log.Println(“start”) g() // possible runtime-error }
这种defer-panic-recover相结合的方式,类似于java中的throw-catch。
错误案例
结构体指针数组初始化时未分配内存
如果是结构体指针数组,直接通过下标来访问结构体会造成panic,因为这些结构体都是nil,这种情况下需要在外构造一个结构体对象c,而后将c的地址传送进去,如arr[0]=c;
如果是结构体数组,则可以直接赋值;
变量在函数外的声明和赋值
正确的示范:
var a int // 函数体外声明,函数内赋值 var b int= 2 // 声明即初始化 //var b = 2 //这样也是可以的 c :=4 // 不能使用这种方式
疑难答疑
这个栏目主要是提供一些常见问题的博客链接,有助于更深入理解Go语言,同时应对各种面试问题。
golang中如何表示枚举类型?
通过iota以及类型别名(如Type State int)来实现枚举
defer和return谁先执行?多个defer是什么顺序?
defer先于return执行,因此其中的操作可能对返回值有影响
- 是否影响取决于命名返回值和匿名返回值,见后
-
具名返回值、匿名返回值对defer有什么影响?
匿名返回值时,defer中的操作不会对返回结果造成影响,反之具名返回值则可以
原因在于,如果不是具名返回值,return时会生成一个临时变量,而后将返回值拷贝给临时变量,之后再进行defer的执行,自然不会影响return的值
golang的struct里面嵌入interface?
golang拾遗:嵌入类型是什么?
golang 匿名结构体成员,具名结构体成员,继承,组合
-
nil 是什么?
type不是一个关键词而是一个预留的标识符(Type类型的变量)
nil只能和pointer, channel, func, interface, map, or slice type进行赋值和比较
Go Channel 详解
发送、接收、阻塞、nil channel
-
序列化和反序列化:omitempty 总结
omitempty字段可以在进行Marshal的时候忽略未赋值或为0值的参数
- 对于不同的基础类型,它零值、默认值和值为0的区别
指针值在序列化时会编码成其指向的值(https://www.cnblogs.com/pangqianjin/p/14403328.html)
字段参数验证库
struct结构体某些字段加的tag:validate,对该字段的值进行验证,比如required表示该值必须,且不为默认值;
-