开启快乐的Go语言之旅吧!
未完待续

入门教程

唯一推荐教程: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有三个特征:

  1. 向有缓存通道写入一个数据总是 happen before这个数据被从通道中读取完成;
  2. 从无缓存通道读取数据 happen before 向通道写入数据完成;
  3. 从容量为 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。

错误案例

结构体指针数组初始化时未分配内存

Golang实战最佳实践 - 图1
如果是结构体指针数组,直接通过下标来访问结构体会造成panic,因为这些结构体都是nil,这种情况下需要在外构造一个结构体对象c,而后将c的地址传送进去,如arr[0]=c;
如果是结构体数组,则可以直接赋值;

变量在函数外的声明和赋值

正确的示范:
var a int // 函数体外声明,函数内赋值 var b int= 2 // 声明即初始化 //var b = 2 //这样也是可以的 c :=4 // 不能使用这种方式

疑难答疑

这个栏目主要是提供一些常见问题的博客链接,有助于更深入理解Go语言,同时应对各种面试问题。

golang中如何表示枚举类型