Go语言工程实践笔记

215 阅读14分钟

并发vs并行

  • 并发(concurrency)是指多个任务可以在同一时间段内交替执行,但不一定同时执行。并发是一种逻辑上的概念,它侧重于任务的切换和调度。
  • 并行(parallelism)是指多个任务可以在同一时刻同时执行。并行是一种物理上的概念,它侧重于任务的执行效率。
  • Go语言支持并发和并行,它提供了goroutine和channel等机制来实现高效的并发编程。
  • 我认为,并发和并行是两个相辅相成的概念,它们都可以提高程序的性能和可扩展性。并发可以让程序充分利用单核或多核的CPU资源,而并行可以让程序在多核或分布式的环境下实现真正的同时执行。

Goroutine

  • Goroutine是Go语言中的轻量级线程,它由Go运行时管理和调度。Goroutine可以看作是一种用户态的线程,它比系统线程更加节省资源和高效。

  • Goroutine的创建和销毁非常快速,它只需要几KB的栈空间,并且可以根据需要动态地增长和缩减。Goroutine之间的切换也非常快速,因为它不需要陷入内核态,而是由Go运行时进行协作式调度。

  • Goroutine可以通过关键字go来创建,例如:go f(x, y, z)会创建一个新的goroutine,并在其中执行函数f。主函数(main goroutine)会继续执行后续的代码,不会等待goroutine结束。

  • 我认为,Goroutine是Go语言最强大和最独特的特性之一,它让并发编程变得非常简单和优雅。Goroutine可以让我们编写出高并发、高性能、高可用的程序,而不需要担心线程的创建、销毁、同步等复杂的细节。

  • Goroutine的创建和使用非常简单,只需要使用关键字go来创建,例如:go f(x, y, z)会创建一个新的goroutine,并在其中执行函数f。主函数(main goroutine)会继续执行后续的代码,不会等待goroutine结束。

  • 以下是一个简单的goroutine示例,它创建了两个goroutine,分别打印出hello和world。注意,由于主函数不会等待goroutine结束,所以需要使用time.Sleep函数来延迟主函数的退出,否则可能看不到goroutine的输出。

package main

import (
	"fmt"
	"time"
)

func main() {
	go say("hello") // 创建一个goroutine,在其中执行say函数
	go say("world") // 创建另一个goroutine,在其中执行say函数
	time.Sleep(1 * time.Second) // 延迟主函数的退出
}

func say(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s) // 打印出参数s
	}
}

CSP(Communicating Sequential Processes)

  • CSP(Communicating Sequential Processes)是一种并发编程模型,它认为并发系统由一组独立的实体(process)组成,这些实体之间通过传递消息(message)进行通信和协作。CSP强调的是“以通信的方式来共享内存”,而不是“以共享内存的方式来通信”。
  • Go语言借鉴了CSP模型的一些思想,并将其实现为goroutine和channel。goroutine代表了CSP中的process,channel代表了CSP中的message。Go语言通过channel来实现goroutine之间的同步和通信,而不需要使用锁、条件变量等传统的并发控制机制。
  • 我认为,CSP模型是一种非常适合分布式系统和云计算场景的并发编程模型,它可以有效地降低并发系统的复杂度和出错率。Go语言通过goroutine和channel提供了一种简洁和高效的CSP编程方式,让我们可以更容易地构建可靠和可扩展的并发系统。

Channel

  • Channel是Go语言中用于在goroutine之间传递数据的一种类型。Channel可以看作是一个管道,数据从一个goroutine流入管道,再从管道流出到另一个goroutine中。Channel可以实现goroutine之间的同步和通信。

  • Channel的创建和使用非常简单,只需要使用内置的make函数来创建一个channel,例如:ch := make(chan int)就创建了一个可以传递int类型数据的channel。然后就可以使用发送操作符<-来向channel发送数据,或者使用接收操作符<-来从channel接收数据,例如:ch <- 3表示向channel发送一个3,x := <- ch表示从channel接收一个数据并赋值给x。

  • Channel有两种类型:无缓冲的channel和有缓冲的channel。无缓冲的channel是指在发送和接收操作之间没有存储空间的channel,这种channel的发送和接收操作必须是同时发生的,否则会导致阻塞。有缓冲的channel是指在发送和接收操作之间有一定容量的存储空间的channel,这种channel的发送和接收操作可以是异步的,只有当缓冲区满了或者空了时才会导致阻塞。有缓冲的channel可以在创建时指定缓冲区的大小,例如:ch := make(chan int, 10)就创建了一个容量为10的有缓冲的channel。

  • 我认为,Channel是Go语言中实现CSP模型的核心类型,它让我们可以用一种简单和安全的方式来在goroutine之间传递数据。Channel可以避免数据竞争和死锁等并发编程中常见的问题,让我们可以更专注于业务逻辑而不是并发控制。

  • Channel的创建和使用非常简单,只需要使用内置的make函数来创建一个channel,例如:ch := make(chan int)就创建了一个可以传递int类型数据的channel。然后就可以使用发送操作符<-来向channel发送数据,或者使用接收操作符<-来从channel接收数据,例如:ch <- 3表示向channel发送一个3,x := <- ch表示从channel接收一个数据并赋值给x。

  • 以下是一个简单的channel示例,它创建了一个无缓冲的channel,并在两个goroutine之间通过channel传递数据。注意,由于无缓冲的channel的发送和接收操作必须是同时发生的,所以需要使用sync.WaitGroup来等待两个goroutine都结束,否则可能导致死锁或者丢失数据。

package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan int) // 创建一个无缓冲的channel
	var wg sync.WaitGroup // 创建一个WaitGroup
	wg.Add(2) // 增加计数器为2
	go func() {
		defer wg.Done() // 函数结束时减少计数器
		for i := 0; i < 5; i++ {
			ch <- i // 向channel发送数据
			fmt.Println("send:", i)
		}
		close(ch) // 关闭channel
	}()
	go func() {
		defer wg.Done() // 函数结束时减少计数器
		for x := range ch { // 从channel接收数据,直到channel关闭
			fmt.Println("receive:", x)
		}
	}()
	wg.Wait() // 等待两个goroutine都结束
}

并发安全 Lock

  • 并发安全(concurrent safety)是指在多个goroutine同时访问或修改同一个共享资源(如变量、结构体、切片、映射等)时,能够保证资源的正确性和一致性的性质。并发安全是并发编程中非常重要的一个概念,它直接影响到程序的正确性和性能。
  • Lock(锁)是一种实现并发安全的机制,它可以保证在任意时刻,只有一个goroutine可以访问或修改共享资源,从而避免数据竞争和不一致等问题。Lock通常是一种互斥锁(mutex),它提供了两个方法:Lock和Unlock。Lock方法用于获取锁,如果锁已经被占用,则会阻塞等待;Unlock方法用于释放锁,如果锁没有被占用,则会导致panic错误。
  • Go语言提供了sync包来实现锁机制,sync包中有两种常用的锁类型:sync.Mutex和sync.RWMutex。sync.Mutex是一个普通的互斥锁,它只提供了Lock和Unlock两个方法;sync.RWMutex是一个读写互斥锁,它提供了四个方法:RLock、RUnlock、Lock和Unlock。读写互斥锁可以允许多个goroutine同时读取共享资源,但只能有一个goroutine写入共享资源。
  • 我认为,Lock是一种比较传统和底层的实现并发安全的机制,它需要我们手动地对共享资源进行加锁和解锁操作,这样可能会增加程序的复杂度和出错率。Go语言通过channel提供了一种更高层次和更优雅的实现并发安全的机制,它让我们可以通过传递数据而不是共享数据来进行并发编程。

WaitGroup

  • WaitGroup是Go语言中用于等待一组goroutine结束的一种类型。WaitGroup可以看作是一个计数器,它提供了三个方法:Add、Done和Wait。Add方法用于增加计数器的值,表示有多少个goroutine需要等待;Done方法用于减少计数器的值,表示有一个goroutine已经结束;Wait方法用于阻塞当前goroutine,直到计数器的值为零,表示所有的goroutine都已经结束。
  • WaitGroup的使用非常简单,只需要在创建goroutine之前调用Add方法,然后在goroutine中的函数结束时调用Done方法,最后在主函数中调用Wait方法。例如:
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup // 创建一个WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1) // 增加计数器
		go func(i int) {
			defer wg.Done() // 函数结束时减少计数器
			fmt.Println(i) // 打印出参数i
		}(i)
	}
	wg.Wait() // 等待所有的goroutine都结束
}
  • 我认为,WaitGroup是Go语言中一个非常实用和方便的类型,它让我们可以更简单和更安全地等待一组goroutine的结束。通过使用WaitGroup,我们可以避免使用time.Sleep或者channel等其他方式来同步goroutine,从而提高代码的可读性和可维护性。

Go 依赖管理演进(GOPATH,GO Vendor,Go Module)

  • Go 依赖管理(dependency management)是指在Go语言中如何管理和使用外部的代码库(package)的问题。Go 依赖管理是一个非常重要的话题,因为它直接影响到代码的可复用性、可维护性、可测试性和可部署性。
  • Go 依赖管理经历了三个阶段:GOPATH、GO Vendor和Go Module。这三个阶段分别代表了Go语言在不同的版本和时期对依赖管理的不同的解决方案。
  • GOPATH是Go语言最早的依赖管理方案,它要求所有的Go代码都必须放在一个特定的目录下(默认为$HOME/go),并且遵循一定的目录结构。GOPATH方案有一些弊端,例如:不能支持多个版本的同一个package,不能支持跨项目的package复用,不能支持离线开发等。
  • GO Vendor是Go语言在1.5版本引入的一个新特性,它允许开发者将项目所依赖的package复制到项目本地的vendor目录下,并且优先使用vendor目录下的package。GO Vendor方案解决了一些GOPATH方案的问题,例如:可以支持跨项目的package复用,可以支持离线开发等。但是GO Vendor方案也有一些弊端,例如:不能支持多个版本的同一个package,不能自动管理package之间的依赖关系,需要额外的工具来维护vendor目录等。
  • Go Module是Go语言在1.11版本引入的一个新特性,它是目前Go语言官方推荐的依赖管理方案。Go Module方案摒弃了GOPATH和GO Vendor方案对目录结构和位置的限制,允许开发者在任意目录下创建和使用Go项目,并且可以自动管理和记录项目所依赖的package及其版本。Go Module方案解决了GOPATH和GO Vendor方案的大部分问题,例如:可以支持多个版本的同一个package,可以自动管理package之间的依赖关系,可以使用代理服务器来加速下载package等。
  • 我认为,Go Module方案是Go语言在依赖管理方面取得的一个重大进步,它让我们可以更灵活和更高效地开发和使用Go项目。Go Module方案也有一些不足之处,例如:需要适应一些新的概念和命令,需要注意一些兼容性和迁移性的问题等。但是我相信随着Go语言的不断发展和完善,Go Module方案会越来越成熟和稳定。

GOPATH(GOPATH-弊端)

  • GOPATH是指一个环境变量,它表示Go代码所在的根目录。GOPATH默认为$HOME/go,在Windows系统下为%USERPROFILE%\go。GOPATH可以包含多个路径,用冒号(:)或分号(;)分隔。

  • GOPATH要求所有的Go代码都必须放在GOPATH/src下,并且遵循以下目录结构:

    • src: 存放源代码文件(.go)、包(package)和项目(project)。
    • pkg: 存放编译后生成的包文件(.a),用于链接。
    • bin: 存放编译后生成的可执行文件(.exe),用于运行。
  • GOPATH有以下几个弊端:

    • 不能支持多个版本的同一个package,因为同一个package只能有一个路径和一个包文件。
    • 不能支持跨项目的package复用,因为不同的项目可能有不同的GOPATH,导致package的路径和版本不一致。
    • 不能支持离线开发,因为需要通过go get命令从远程仓库下载package,而且可能会受到网络和墙的影响。
    • 我认为,GOPATH是一种过于简单和粗暴的依赖管理方案,它没有考虑到Go语言在实际开发中遇到的各种复杂和多变的情况。GOPATH限制了Go语言的发展和应用,让很多开发者感到困惑和不满。

GO Vendor(GO Vendor-弊端)

  • GO Vendor是指一个特殊的目录,它表示项目所依赖的package的本地副本。GO Vendor可以存在于任意一个Go项目的根目录下,它的名称必须为vendor。GO Vendor可以通过手动或者使用一些工具(如govendor、glide、dep等)来创建和维护。

  • GO Vendor的作用是让Go编译器在查找package时,优先使用vendor目录下的package,而不是GOPATH下的package。这样可以保证项目使用的package是固定和一致的,而不会受到外部因素的影响。

  • GO Vendor有以下几个弊端:

    • 不能支持多个版本的同一个package,因为同一个package只能有一个路径和一个包文件。
    • 不能自动管理package之间的依赖关系,因为vendor目录只是一个简单的文件夹,没有记录package的元信息(如版本、来源、依赖等)。
    • 需要额外的工具来维护vendor目录,因为手动管理vendor目录非常繁琐和容易出错。
    • 我认为,GO Vendor是一种比GOPATH稍微进步一点的依赖管理方案,它解决了一些GOPATH方案无法解决的问题,但是也带来了一些新的问题。GO Vendor仍然没有提供一种完善和统一的依赖管理机制,让很多开发者感到不便和不安。

Go Module

  • Go Module是指一种新的依赖管理机制,它在Go语言1.11版本中引入,并在1.13版本中成为默认选项。Go Module可以让开发者在任意目录下创建和使用Go项目,并且可以自动管理和记录项目所依赖的package及其版本。

  • Go Module主要由以下几个部分组成:

    • module: 一个module是一个包含了一组相关Go代码文件(.go)和一个go.mod文件(用于记录module信息)的目录。一个module可以是一个单独的package,也可以是一个包含了多个子package(sub-package)的项目。一个module通常对应于一个代码仓库(repository),例如:github.com/user/project。
    • package: 一个package是一个包含了一组相关Go代码文件(.go)和一个可选的go.sum文件(用于记录package校验和)的目录。一个package必须属于某个module,并且可以被其他module引用和使用。一个package通常对应于一个代码目录(directory),例如:github.com/user/project/pkg。
    • dependency: 一个dependency是指一个module所依赖或者引用的其他module或者package。每个dependency都有一个唯一的路径(path)和一个指定的版本(version)。dependency可以通过go get命令来下载或者更新,并且会被记录在go.mod文件中。
    • version: 一个version是指一个module或者package在某个特定时间点或者状态下的标识符。

依赖管理三要素

  • 依赖管理三要素是指在Go Module方案中,用于描述和控制module和package之间的依赖关系的三个重要的概念:依赖配置(dependency configuration)、依赖分发(dependency distribution)和工具(tool)。
  • 依赖配置是指用于记录module和package的元信息(如路径、版本、来源、依赖等)的文件或者数据结构。Go Module方案中,主要有两个依赖配置文件:go.mod和go.sum。
  • 依赖分发是指用于下载或者更新module和package的方式或者渠道。Go Module方案中,主要有两种依赖分发方式:回源(direct)和代理(proxy)。
  • 工具是指用于创建、管理、使用和维护module和package的命令或者程序。Go Module方案中,主要有两个工具:go get和go mod。
  • 我认为,依赖管理三要素是Go Module方案的核心组成部分,它们共同构成了一个完整和统一的依赖管理机制。通过了解和掌握这三个要素,我们可以更好地理解和使用Go Module方案。

依赖配置 - go.mod

  • go.mod是一个用于记录module信息的文本文件,它位于module的根目录下,它的名称必须为go.mod。go.mod文件可以由开发者手动编写,也可以由go mod命令自动生成或者修改。

  • go.mod文件主要包含以下几个部分:

    • module: 声明当前module的路径(path),例如:module github.com/user/project
    • go: 声明当前module使用的Go语言版本(version),例如:go 1.16
    • require: 声明当前module所依赖或者引用的其他module或者package及其版本(version),例如:require github.com/pkg/errors v0.9.1
    • replace: 声明当前module对某些dependency的替换规则(rule),例如:replace github.com/pkg/errors v0.9.1 => github.com/pkg/errors v0.8.0
    • exclude: 声明当前module对某些dependency的排除规则(rule),例如:exclude github.com/pkg/errors v0.9.1
  • 我认为,go.mod文件是Go Module方案中最重要和最常用的依赖配置文件,它记录了当前module的基本信息和依赖关系。通过阅读和编辑go.mod文件,我们可以清楚地了解和控制当前module的状态和行为。