Go(又称 Golang)是 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。
基础
关键字
Go 是一门类似 C 的编译型语言,但是它的编译速度非常快。
这门语言的关键字总共也就二十五个:
1 | break default func interface select |
变量
一个特定的名字与位于特定位置的内存块绑定在一起,这个名字被称为变量。
声明变量的一般形式是使用 var
关键字:var identifier type
变量的类型放在变量的名称之后,是为了避免像 C 语言中那样含糊不清的声明形式。
C++:int* a, b; // a 是指针,b 不是
Go:var a,b *int // a,b 都是指针
1 | var a int |
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写
如果全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写
值类型
- 所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
- 像数组和结构体这些复合类型也是值类型
- 当使用等号
=
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝 - 可以通过
&i
来获取变量 i 的内存地址(每次的地址都可能不一样),值类型的变量的值存储在栈中
引用类型
- 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置(这个内存地址被称之为指针)
- 在 Go 语言中,指针、slices(切片)、maps 和 channel 都属于引用类型
- 当使用赋值语句
r2 = r1
时,只有引用(地址)被复制 - 当 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容
- 被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间
常量
Go 语言的常量是一种在源码编译期间被创建的语法元素。
1 | const Pi float64 = 3.14159265358979323846 // 单行常量声明,显示 |
常量的类型只局限于基本数据类型,包括数值类型、字符串类型、布尔类型
Go 语言在常量方面的创新包括下面这几点:
支持无类型常量:
可以不显示指定类型,比如
const n = 13
支持隐式自动转型:
对于无类型常量参与的表达式求值,Go 编译器会根据上下文中的类型信息,把无类型常量自动转换为相应的类型后,再参与求值计算,这一转型动作是隐式进行的
可用于实现枚举:
隐式重复前一个非空表达式
1
2
3
4
5const (
Apple, Banana = 11, 22
Strawberry, Grape = 11, 22 // 使用上一行的初始化表达式
Pear, Watermelon = 11, 22 // 使用上一行的初始化表达式
)iota 是一个预定义标识符,可以从 0 开始自增(位于同一行的 iota 即便出现多次,多个 iota 的值也是一样的)
1
2
3
4
5
6
7
8const (
Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)
Strawberry, Grape // 1, 11 (iota = 1)
Pear, Watermelon // 2, 12 (iota = 2)
)
// 如果想从 1 开始
// _ = iota // 0
// 每遇到一次 const 关键字,iota 就重置为 0
数组
数组是一个长度固定的、由同构类型元素组成的连续序列,包含两个重要属性:元素的类型和数组长度(元素的个数)
数组变量声明:
1 | var arr [5]int // 一维 |
数组类型变量是一个整体,这就意味着一个数组变量表示的是整个数组。
这点与 C 语言完全不同,在 C 语言中,数组变量可视为指向数组第一个元素的指针。
切片
在 Go 语言中,数组更多是“退居幕后”,承担的是底层存储空间的角色。
切片就是数组的“描述符”,也正是因为这一特性,切片才能在函数参数传递时避免较大性能开销。可以说,切片之于数组就像是文件描述符之于文件。
去掉“长度”这一束缚后,切片展现出更为灵活的特性
1 | var nums = []int{1, 2, 3, 4, 5, 6} |
底层实现
切片的底层数据结构:在运行时其实是一个三元组结构
1 | type slice struct { |
Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长度与切片初始元素个数相同
创建
切片的创建根据情况不同,主要通过以下 3 种方法创建:
1 | // 1、make 函数 |
动态扩容
当通过 append 操作向切片追加数据的时候,如果这时切片的 len 值和 cap 值是相等的,也就是说切片底层数组已经没有空闲空间再来存储追加的值了,Go 运行时就会对这个切片做扩容操作,来保证切片始终能存储下追加的新值。
1 | var s []int |
Map
map 是 Go 语言提供的一种抽象数据类型,它表示一组无序的键值对。
形式:map[key_type]value_type
Go 语言中要求,key 的类型必须支持“==”和“!=”两种比较操作符。
函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的。
底层实现
Go 运行时使用一张哈希表来实现抽象的 map 类型。
运行时实现了 map 类型操作的所有功能,包括查找、插入、删除等。
在编译阶段,Go 编译器会将 Go 语法层面的 map 操作,重写成运行时对应的函数调用。
1 | // 创建map类型变量实例 |
hmap
类型是 map
类型的头部结构(header),之前提到的 map 类型的描述符,它存储了后续 map 类型操作所需的所有信息
不要依赖 map 的元素遍历顺序;
map 不是线程安全的,不支持并发读写;
不要尝试获取 map 中元素(value)的地址
map 扩容:
当 count > LoadFactor * 2^B 或 overflow bucket 过多时,运行时会自动对 map 进行扩容( Go 最新 1.17 版本 LoadFactor 设置为 6.5)
初始化
切片类型,初值为零值 nil 的切片类型变量,可以借助内置的 append 的函数进行操作,这种在 Go 语言中被称为“零值可用”
map 类型,因为它内部实现的复杂性,无法“零值可用”
1 | var m map[string]int // m = nil |
基本操作
和切片类型一样,map 也是引用类型。
这就意味着 map 类型变量作为参数被传递给函数或方法的时候,实质上传递的只是一个“描述符”,而不是整个 map 的数据拷贝,所以这个传递的开销是固定的,而且也很小。
1 | // 插入,更新相同 |
控制结构
If-else
1 | if condition1 { |
Switch
1 | switch var1 { |
For
Go 语言不提供 while,所以 for 循环更加强大
1 | for i := 0; i < 5; i++ { |
For-range
遍历数组、切片、字符串、map 等的好帮手
1 | for i, v := range sl { |
break、continue 与其他语言功能相同
label +goto 会造成可读性极差,不推荐使用(基本没看到人用)
函数
Go 函数支持多返回值,函数定义一般如下形式:
1 | func funcName(参数列表) 返回值列表 |
函数在 Go 语言中属于“一等公民(First-Class Citizen)”:
- Go 函数可以存储在变量中,且拥有自己的类型
1 | var dfs func(index int) // 函数类型 |
- 支持在函数内创建并通过返回值返回
1 | func dfs(task string) func() { |
- 作为参数传入函数
1 | time.AfterFunc(time.Second*2, func() { println("timer fired") }) |
参数传递
按值传递
call by value
Go 默认使用按值传递来传递参数,也就是传递参数的副本(逐位拷贝)。
- 函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量
引用传递
call by reference
如果希望函数可以直接修改参数的值,而不是对参数的副本进行操作,
需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是引用传递。
- 传递指针(一个 32 位或者 64 位的值)的消耗都比传递副本来得少
变长参数
如果函数的最后一个参数是采用 …type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
1 | func Greeting(prefix string, who ...string) |
命名返回值
尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。
1 | func getX2AndX3(input int) (int, int) { // 非命名 |
Defer
defer 是 Go 语言提供的一种延迟调用机制,defer 的运作离不开函数。
在 Go 中,只有在函数(和方法)内部才能使用 defer
defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。
defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行。

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数。
所以,deferred 函数是一个可以在任何情况下为函数进行收尾工作的好“伙伴”。
1 | // 关闭文件流 |
跟踪
使用 defer 可以跟踪函数的执行过程
1 | func Trace(name string) func() { |
输出:
1 | enter: main |
内置函数
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于数组、切片和管道,不能用于 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它的括号 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数(详见第 4.2 节),在部署环境中建议使用 fmt 包 |
complex、real imag | 用于创建和操作复数(详见第 4.5.2.2 节) |
结构与方法
Struct
结构体定义的一般方式如下:
1 | type identifier struct { |
type T struct {a, b int}
也是合法的语法,它更适用于简单的结构体。
初始化
1 | // 1-struct as a value type: |
Method
在 Go 语言中,结构体就像是类的一种简化形式,那么类的方法在哪里呢?
Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
定义方法的一般格式如下:
1 | func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } |
- 方法声明要与 receiver 参数的基类型声明放在同一个包内。(即 struct 与 method 在同一个 package)
- receiver 参数的基类型本身不能为指针类型或接口类型
1 | type TwoInts struct { |
Receiver 参数的选择
- 如果 Go 方法要把对 receiver 参数代表的类型实例的修改,反映到原类型实例上,那么我们应该选择 *T 作为 receiver 参数的类型
- 如果 receiver 参数类型的 size 较大,以值拷贝形式传入就会导致较大的性能开销,这时我们选择 *T 作为 receiver 类型可能更好些
- T 类型是否需要实现某个接口,也就是是否存在将 T 类型的变量赋值给某接口类型变量的情况。
- 如果需要,那使用 T 作为 receiver 参数的类型,来满足接口类型方法集合中的所有方法
- 如果 T 不需要,但
*T
需要,*T
的方法集合是包含 T 的方法集合的,参考上面 2 个原则选择
总结:
- 类型 T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
- 类型 *T 的可调用方法集包含接受者为 *T 的所有方法
- 类型 *T 的可调用方法集不包含接受者为 T 的方法
接口与反射
Interface
Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。
接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:
1 | type Namer interface { |
上面的 Namer
是一个 接口类型。
命名:
- 接口的名字由方法名加
er
后缀组成 - 当后缀
er
不合适时,以able
结尾或者以I
开头,比如Recoverable
通常它们会包含 0 个、最多 3 个方法(小接口,抽象程度高)
空接口
如果一个类型 T 的方法集合是某接口类型 I 的方法集合的等价集合或超集,类型 T 实现了接口类型 I,
那么类型 T 的变量就可以作为合法的右值赋值给接口类型 I 的变量。
空接口类型的这一可接受任意类型变量值作为右值的特性,是 Go 加入泛型语法之前唯一一种具有“泛型”能力的语法元素
1 | var i interface{} = 15 // ok |
类型断言
一个接口类型的变量 varI
中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。
在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。
类型 T
的值:
1 | v := varI.(T) // unchecked type assertion,varI 必须是一个接口变量 |
- 如果断言成功,变量 v 的类型为 i 的值的类型,而并非接口类型 T
- 如果断言失败,v 的类型信息为接口类型 T,它的值为 nil
类型判断
接口变量的类型也可以使用一种特殊形式的 switch
来检测:type-switch
1 | switch t := areaIntf.(type) { |
Reflection
反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。
反射可以在运行时检查类型和变量,例如它的大小、方法和 动态
的调用这些方法。
这对于没有源代码的包尤其有用,除非真得有必要,否则应当避免使用或小心使用。
变量的最基本信息就是类型和值:反射包的 Type
用来表示一个 Go 类型,反射包的 Value
为 Go 值提供了反射接口。
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。
1 | func TypeOf(i interface{}) Type |
更多查看
反射包 unknwon/the-way-to-go_ZH_CN · GitHub
Go 中的面向对象
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:
1)包范围内的:通过标识符首字母小写,
对象
只在它所在的包内可见2)可导出的:通过标识符首字母大写,
对象
对所在包以外也可见
类型只拥有自己所在包中定义的方法。
- 继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
- 多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
并发
并发是一种能力,它让你的程序可以由若干个代码片段组合而成,并且每个片段都是独立运行的。
Go 并没有使用操作系统线程作为承载分解后的代码片段(模块)的基本执行单元,而是实现了goroutine这一由 Go 运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。
Goroutine
相比传统操作系统线程来说,goroutine 的优势主要是:
- 资源占用小,每个 goroutine 的初始栈大小仅为 2k
- 由 Go 运行时而不是操作系统调度,goroutine 上下文切换在用户层完成,开销更小
- 在语言层面而不是通过标准库提供。goroutine 由go 关键字创建,一退出就会被回收或销毁,开发体验更佳
- 语言内置 channel 作为 goroutine 间通信原语,为并发设计提供了强大支撑。
创建
Go 语言通过go关键字 +函数/方法
的方式创建一个 goroutine。
创建后,新 goroutine 将拥有独立的代码执行流,并与创建它的 goroutine 一起被 Go 运行时调度。
1 | go fmt.Println("I am a goroutine") |
不需要考虑对 goroutine 的退出进行控制:goroutine 的执行函数的返回,就意味着 goroutine 退出。
如果需要手动控制,通过 channel
实现
Channel
传统语言的并发模型是基于对内存的共享的
传统的编程语言(比如:C++、Java、Python 等)并非面向并发而生的,所以他们面对并发的逻辑多是基于操作系统的线程。
并发的执行单元(线程)之间的通信,利用的也是操作系统提供的线程或进程间通信的原语,
比如:共享内存、信号(signal)、管道(pipe)、消息队列、套接字(socket)等。
channel 既可以用来实现 Goroutine 间的通信,还可以实现 Goroutine 间的同步。
创建
和切片、结构体、map 等一样,channel 也是一种复合数据类型。
channel 类型变量赋初值的唯一方法是使用 make 。
1 | var ch chan int // 声明 int 类型的 channel 类型变量,默认值为 nil |
发送和接收
Go 提供了 <-
操作符用于对 channel 类型变量进行发送与接收操作:
1 | ch1 <- 13 // 将整型字面值13发送到无缓冲channel类型变量ch1中 |
- 无缓冲 channel:发送与接收操作同步,一定要放在两个不同的 Goroutine 中进行,否则会导致 deadlock
- 带缓冲 channel:发送或接收不需要阻塞等待,异步
1 | ch2 := make(chan int, 1) |
关闭
调用 go 内置的 close()
函数,发送端负责关闭 channel
1 | close(ch) |
Select
通过 select,我们可以同时在多个 channel 上进行发送 / 接收操作:
1 | select { |
channel 和 select 的结合使用能形成强大的表达能力:
- 利用 default 分支避免阻塞
- 实现超时机制
- 实现心跳机制
Len
len 是 Go 语言的一个内置函数,它支持接收数组、切片、map、字符串和 channel 类型的参数,并返回对应类型的“长度”,也就是一个整型值。
针对 channel ch 的类型不同,len(ch) 有如下两种语义:
- 当 ch 为无缓冲 channel 时,len(ch) 总是返回 0
- 当 ch 为带缓冲 channel 时,len(ch) 返回当前 channel ch 中尚未被读取的元素个数
应用
无缓冲
无缓冲 channel 兼具通信和同步特性,在并发程序中应用颇为广泛。
- 信号传递
- 替代锁机制
带缓冲
带缓冲的 channel 与无缓冲的 channel 的最大不同之处,就在于它的异步性。
对一个带缓冲 channel:
- 在缓冲区未满的情况下,对它进行发送操作的 Goroutine 不会阻塞挂起
- 在缓冲区有数据的情况下,对它进行接收操作的 Goroutine 也不会阻塞挂起
所以可以用于:
- 消息队列
- 计数信号量
常用包
Strings
作为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数。Go 中使用 strings
包来完成对字符串的主要操作。
strings package - strings - pkg.go.dev
常用函数:
1 | // 判断前缀 |
Strconv
与字符串相关的类型转换都是通过 strconv
包实现的。
strconv package - strconv - pkg.go.dev
常用函数:
1 | strconv.IntSize // 当前操作系统 int 所占位置 |
Time
time
包为我们提供了一个数据类型 time.Time
(作为值使用)以及显示和测量时间和日期的功能函数。
time package - time - pkg.go.dev
常用函数:
1 | t := time.Now() // 当前时间 |