Go 语言的设计目的是作为 C/C++ 的替代工具,是谷歌内部为了解决 C/C++ 开发中的一些痛点:编译慢、不受控制的依赖、三方库混乱、编程范式繁多(每个人都选择其中一部分特性)、并发编程麻烦、跨平台代码兼容、内存安全、工程开发效率低等等
Go的优势
- 静态编译,编译速度快
- 云原生+跨平台
- 独特的异步编程机制
- 垃圾回收
接下来我们从以下几方面来入门Go语言,帮助大家开始尽早进入开发流程:
Go是一门非常简约时尚的语言,希望我的排版也可以带给大家简约的美感
Go是一门支持函数式编程的强类型语言
常用的基本类型:bool,int,float,struct,string,slice,map,channel,array,interface(接口),function(函数)
Go存在基本类型与引用类型的划分,string为基本类型,其默认值为“”
1 | package main |
声明变量,类型后置
1 | var a,b bool |
1 | var b = true |
Tip: 相同的代码块中,不可以再次对于相同名称的变量使用初始化声明
声明常量并立即赋值,只可以是布尔型、数字型和字符串型
1 | const d = 2525 |
常量用作枚举,可以使用内置函数赋值
iota常量生成器,在 const 关键字出现时将被重置为 0,const 中每新增一行常量声明将使 iota 计数一次
1 | const ( |
1 | const ( |
与c语言类似,添加**
用作乘方运算符
不推荐使用括号包裹条件,支持在if内执行初始化语句
1 | if err := file.Chmod(0664); err != nil { |
支持对确定类型的值进行选择,无switch穿透,使用fallthrough
进入下一分支
1 | switch c { |
1 | var t interface{} |
通道选择,执行select时,会同时监听多个通道的操作
支持三种表达式:即通道接收表达式,通道发送表达式和default,所有发送表达式在select前被并行求值(即使其中某些表达式可能永远不会被执行)
1 | select { |
若有通道准备完毕,则执行对应代码块并结束。若已有多个通道准备完毕,则随机选择一个执行。若所有通道未准备好,则执行default代码或被阻塞
循环语句,支持使用break暂停并跳出循环,支持使用continue跳转到下一次循环,支持标记特定循环
1 | for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { |
支持自动获取遍历迭代器
1 | for key, value := range oldMap { |
函数,参数存在值传递与引用传递,并支持返回多个值,返回值可以命名
1 | func ReadFull(r Reader, buf []byte) (n int, err error) { |
为了实现多值返回,Go是使用栈空间来返回值的,实际上是在传入的参数之上留了两个空位,被调者直接将返回值放在这两空位。而C语言是通过寄存器来返回值的。
支持匿名函数和闭包,函数可以在其他函数内部定义,所有函数内可以访问其外部函数的变量,并且在不同的作用域中保持对这些变量的引用
func是一个值,可以作为参数传递
1 | func main() { |
结构体,由各类字段组成的复合结构,共有字段名称首字母大写,私有小写
对结构体变量或指针使用.
操作符访问其字段,支持为其字段赋值或对其字段进行取址(&)操作
1 | type SyncedBuffer struct { |
初始化支持自定义构造函数,使用时可以按照顺序的全参构造,也可使用键值对样式的部分参数构造
除使用构造函数外,也可以使用Go内置函数new()
分配内存,初始化结构体并返回分配的内存指针
1 | buffer := SyncedBuffer{nil,nil} |
有序序列,长度固定,与C不同的是将 []
前置。
数组支持栈分配和值传递,效率更高。不支持引用传递,通常使用切片作为参数传递
1 | arr := [5]int{} |
需要注意海象符的语义:带类型推导的赋值,而非声明某类型的变量;因此
arr := [5]int
和i := int
一样不合法
通过 []
来访问元素,不支持数组指针
1 | array[0] = 3 |
有序序列,长度可变,与数组类似,但为引用传递,堆上分配
切片支持单独构造,也可以作为数组的引用,用来间接操作数组
1 | s := []int{} |
1 | array := [5]int{1, 2, 3, 4, 5} |
通过 []
来访问元素,使用 append()
函数添加元素,使用切片操作进行截取
1 | sub := slice[1:3]//[2,3] |
映射是一个无序的键值对集合,可以根据键来快速检索对应的值
1 | m := map[string]int{"key":1} |
map 的底层实现是一个哈希表数据结构,使用一个哈希函数来计算键的哈希值,然后将键值对存储在一个称为桶(bucket)的数组中。每个桶包含一个链表或者红黑树,用于解决哈希冲突
支持添加值,获取值并判断是否存在,range获取迭代器,delete删除键值对
1 | m["key"] = 2 |
如果从一个未初始化的 map 中获取值,将会返回该值类型的零值
channel是一种可以用来在协程之间传递数据的类型,channel是线程安全的,多个协程可以同时读写一个channel,而不会发生数据竞争的问题
使用make创建channel,使用close关闭channel
1 | ch := make(chan string) |
使用通道符号 <-
接收或发送信息,分为通道接收表达式 <-channel
和通道发送表达式 channel <- value
1 | ch <- "Hello" |
channel可以带有一个缓冲区,用于存储一定量的数据。如果缓冲区已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据;如果缓冲区已经空了,接收操作会被阻塞,直到有其他协程向channel中发送了数据。
1 | ch := make(chan int, 10) |
支持配合select在多个goroutine间传递数据
方法是与特定类型相关联的函数,可以将函数与特定类型绑定在一起,使得该类型的对象可以调用该方法
方法可以在任何自定义类型上定义,包括结构体、基本类型的别名类型以及接口,只需在函数名前面加上接收者,
1 | func (receiver) method(params) (results) { |
接收者可以为一个值或一个指针,也可以只有类型
1 | type Person struct{} |
接口是一种抽象类型,它定义了一组方法签名,方法签名包括方法的名称、参数列表和返回值列表
1 | type Writer interface { |
类型只需要实现了接口中定义的所有方法,就被视为实现了该接口
1 | type ConsoleWriter struct{} |
若存在匿名字段,其的方法可以被外部直接调用,通过组合与特殊语法实现了类似继承的机制
注意,只有方法被继承,且支持一个类型拥有多个匿名字段,所有匿名字段的方法都被继承
1 | type Person struct {} |
类型断言用于检查变量是否为某种类型,并返回基础接口值。类型断言仅适用于接口
1 | value, ok := variable.(Type) |
类型转换用于将一个类型的值转换为另一个类型,只有在两种类型之间存在兼容关系时,才能进行类型转换
1 | value := Type(variable) |
interface{}
可以代表动态类型,从逻辑上看,所有类型都实现了空接口
struct{}
是一个特殊的类型,一个不包含任何字段的空结构体类型,其值没有字段需要存储,不占用任何内存空间。可以创建空结构体的实例,并将其传递给函数或通过通道进行发送,来表示某种特定的事件或触发某些操作,而不需要传递具体的数据
defer函数被延迟到函数运行结束后,主要用来释放资源。若有多条defer函数则按LIFO顺序(后进先出)执行
1 | func trace(s string) { fmt.Println("entering:", s) } |
结构体字段结尾使用 “tag” 为字段附加元数据,可以在运行时通过反射机制读取,常用于实现对象的序列化、数据校验等
1 | type Person struct { |
与其他语言类似的语法糖,可变长参数会被作为切片(slice)传递给函数
1 | func sum(nums ...int) int { |
1 | result := sum(1, 2, 3, 4) |
使用反引号(`)来定义。在原始字符串字面量中,特殊字符(如反斜杠和换行符)将被直接包含,无需进行转义
1 | str := `This is a raw string literal. |
Go 的官方教程非常全面,建议大家有空可以读读,可以在A Tour of Go敲一敲代码,Or 直接官方文档Documentation - The Go Programming Language
我是一只可爱的留白喵
工程化是构建一门强大的开发语言和完整生态系统的重要组成部分
除语言设计本身外,代码规范和风格,依赖管理,构建和部署等对实现工程化也起着至关重要的作用
需要遵守特定的命名规范,使代码更易读并符合 Go 社区的惯例:
er
结尾,以表示它是一个接口。err
作为前缀,这样可以清晰地表示该变量是用来存储错误信息的。按照字母顺序对导入包进行分组,并在每个分组中按照标准库包、第三方库包和本地包的顺序排序。这种组织方式有助于提高代码的可读性,并且便于查找和维护导入的包。
使用 //
进行单行注释,使用 /* */
进行块注释。对于函数、方法和类型,使用注释来解释其功能和参数含义。对于公共的函数和方法,使用注释来描述其作用、返回值和可能的错误。注释对于代码的理解和维护非常重要。
遵循 Go 语言的错误处理机制,使用多返回值来返回函数的结果和错误信息。对于可能出现错误的地方,及时检查和处理错误。这样可以提高代码的健壮性和可靠性。
将相关的类型、函数和常量放在同一个包中,并将它们放在一个逻辑上相关的文件中。这种组织方式有助于代码的模块化和可维护性。对外提供的公共函数和类型应以大写字母开头,非公共的函数和类型应以小写字母开头。
尽量避免使用全局变量,而是使用函数的参数和返回值来传递数据。这种做法有助于避免不必要的依赖和副作用,使代码更易于测试和维护。
Go对于依赖包的版本管理基于语义化,即版本号需要按照以规定 v<major>.<minor>.<patch>
在Go中存在两个重要目录:GOROOT、GOPATH,分别为Go 语言安装目录和用户工作空间目录
Go 项目由一个或者多个 package 组成,package 按照来源可能分为:标准库、第三方库、项目私有库。标准库全部位于 GOROOT 目录中,而第三方库和私有库,都位于 GOPATH 目录
但是GoPath有一个非常严重的问题,就是所有的依赖的包必须由程序员手动管理,项目之间的版本管理没有分离,非常混乱,所以Go官方推出了依赖管理工具GoModule
使用命令 go mod init [module name]
初始化项目
GoModule主要为了解决依赖管理的问题,自动管理解决和管理依赖冲突,并实现了构建功能:
在使用 Go Modules 进行依赖管理时,下载的依赖包会被存储在项目下的 pkg/mod
缓存目录。这样,每个项目都有其独立的依赖包目录,不会与其他项目共享依赖
通常我们直接将依赖声明在go.mod中,由以下几部分组成:
repo + module name
的方式定义1 | module github.com/feellmoose/gotest |
除了直接修改 go.mod
外,我们还可以使用以下命令更加方便的管理:
go get [package]
添加新的依赖包到项目中,自动下载并将其添加到go.mod
文件中go get -u [package]
更新项目中的依赖包,自动下载最新版本的依赖并更新go.mod
文件go mod tidy
移除不再使用的依赖包,根据项目代码的实际依赖情况自动更新go.mod
文件go list -m all
查看项目的所有依赖及其版本,列出项目直接和间接依赖的所有模块go clean -modcache
命令清理缓存,以释放磁盘空间或解决依赖包下载问题使用 go build
通过GoModule简单构建可执行文件,第一次构建时会自动生成 go.sum 文件,其中有多行记录,每行记录由包名、版本号、哈希值组成,使用空格分开,第一次构建时生成的哈希值需要经过校验
当之后构建项目时,Go 会先从缓存中获取依赖包,然后计算本地依赖包的哈希值,和 go.sum 中的哈希值对比,如果不一致,就会拒绝构建
除了简单构建可执行文件以外,Go通过交叉编译支持跨平台构建,通过参数GOOS
和GOARCH
,可以指定构建的目标平台,例如GOOS=windows GOARCH=amd64 go build
Go支持静态编译,将全部依赖包编译入可执行文件,便于直接部署。使用-ldflags
标志来指定静态编译的参数,例如 go build -ldflags "-linkmode external -extldflags '-static'"
打包好的可执行文件可以直接部署
我是一只可爱的留白喵
在进阶知识一章中,我将为大家继续补充其他有用的知识
分号的隐式使用:分号是可选的,编译器会根据换行符自动插入分号,这使得多行调用产生了歧义,于是以下两条看起来不合理的做法合理了起来
点号的位置限制:多行调用的点号 .
必须在前一行的末尾,而不能单独占据一行,否则会产生歧义
大括号的位置限制:左大括号 {
必须与控制结构声明在同一行,且右大括号 }
必须在下一行的开头,或者与前一行的代码在同一列,否则会产生歧义
虽然写Go的程序员们都说这些语法上的限制和约定是为了确保代码的可读性、一致性和语法的简洁性。
非常的简单,非常的美味,下列代码提供了中序遍历,其他遍历规则只需调整最后三行代码的顺序
1 | type Node struct { |
GoModule有一个非常棒的特性,就是其支持直接将代码托管平台作为仓库,从这些仓库直接获取依赖的包,所以当我们想要发布一个包时,可以直接使用GitHub发布和分享
使用 github.com/username/projectname
命名包,然后将带有go.mod的源代码推送到username下对应的projectname仓库,此时一个最新版本的包就可以被其他人获取了
其他开发者可以通过在他们的项目中运行go get github.com/username/projectname
获取包,Go工具链会自动下载包及其依赖,并将其放在GOPATH
目录下的合适位置,以供其他项目引用
但是只有一个最新版本是不够的,为了不断更新我们必须产生多个版本并迭代开发,一个解决方案是我们可以使用Git的标签(Tags)来管理版本,每发布一个新的版本时,使用Git标签来标记这个版本,go mod会识别每个tags内对应go.mod的版本并获取对应版本的包
Go具有精简的并发模型,基于CSP,即通信顺序模型。在CSP模型中,独立运行的并发实体通过通信进行交互,而不是通过共享内存。Go中,独立运行的并发实体就是Goroutine,Goroutine间使用channel进行通信
CSP(Communicating Sequential Processes)理论,由计算机科学家Tony Hoare在1977年的论文中提出。
Goroutine是Go语言并发的基本单位。它是一种轻量级线程,由Go运行时(runtime)管理。与传统的操作系统线程相比,Goroutine的启动和销毁的代价非常低,并且可以高效地运行成千上万个Goroutine
通道是Goroutine之间进行通信和同步的机制。它提供了一种安全的方式来传递数据和同步操作,避免了显式的锁和竞态条件。通过通道,Goroutine可以发送和接收数据,以进行协作和共享数据
select语句用于在多个通道操作中进行选择。它允许Goroutine同时等待多个通道操作的就绪状态,并执行相应的操作。select语句是Go语言处理并发多路选择的关键机制之一
在一个Goroutine向另一个Goroutine发送消息时,另一个Goroutine在等待过程中会阻塞(底层表现是协程被挂起),这代表着我们可以基于这一机制完成全部基于同步和锁的异步并发模型,至此结束,一切又回到了起点。(实际上我们也可以使用互斥锁模型完成这一操作)
1 | go func doAsync(done chan struct{}) { |
Go语言的
sync.Mutex
和sync.RWMutex
是使用底层的操作系统原语(如互斥量和条件变量)来实现的,提供了临界区保护和简单资源保护的轻量级选择。在选择合适的同步机制时,需要根据具体的需求、场景和性能考虑做出决策
我们也可以使用WaitGroup来等待某些任务完成,在派发多个任务的时候非常有效
1 | package main |
除了与其他语言类似的同步编程模型,我们还可以利用锁和 channel 的特殊性质,简单实现基于 topic 的发布订阅模型:
1 | type PubSub struct { |
我们利用map存储channel和对应的topic,subscriber可以从对应的channel中获取message
1 | func main() { |
除了go和channel以外,谈及Go并发设计还有另外一个关键字select也经常出现,select主要用于处理多个通信操作的就绪状态,例如处理多个通信操作、超时控制和操作系统发出的退出程序指令
1 | package main |
我是一只可爱的留白喵