Go学习笔记总结!
Go学习笔记总结!
月伴飞鱼基础知识
官方网站:
中文网在线标准库文档:
Google创造Go语言的目的:
计算机硬件技术更新频繁:
- 性能提高很快,目前主流的编程语言发展落后于硬件
- 不能合理利用多核
CPU
的优势提升软件系统性能软件系统复杂度越来越高,维护成本越来越高
- 目前缺乏一个足够简洁高效的编程语言
企业运行维护很多C/C++的项目,C/C++程序运行速度很快,但是编译速度很慢
- 同时还存在内存泄漏的一系列的困扰需要解决
Go岗位
区块链研发工程师
服务器/游戏软件工程师
分布式/云计算软件工程师
Go语言特点
Go语言保证了既能够达到静态编译语言的安全和性能,又达到了动态语言开发类维护的高效率。
GO = C + Python
:
- 说明Go语言既有C静态语言程序的运行速度,又能达到Python的动态语言的快速开发。
天然并发:
从语言层面支持并发,实现简单。
GoRoutine
,轻量级线程,可实现大并发处理,高效利用多核。管道通信机制:
- Go语言特有的管道Channel,通过
Channel
,可以实现不同的GoRoute之间的相互通信。函数返回多个值。
新的创新内容:
- 例如切片、延时执行Defer等。
安装
下载地址:https://go.dev/dl/
- 默认安装路径是
/usr/local/go
配置环境变量:
1 | $ vim ~/.bash_profile |
1 | export GOPATH=/usr/local/go |
1 | $ source ~/.bash_profile |
验证安装:
1 | go version |
第一个Go程序
一个简单的 Go 程序文件
hello.go
。
1 | package main // package main 定义了包名。必须在源文件中非注释的第一行指明这个文件属于哪个包 |
构建该程序:
1 | go build hello.go |
生成一个名为
hello
的可执行文件,可以运行它。
1 | ./hello |
使用
go install
- 不仅构建程序,还将结果文件安装到
GOPATH
指定的目录中,以供全局使用。
- 通常是在
$GOPATH/bin
目录下。- 可以在任何地方运行程序。
1 | go install hello.go |
执行流程分析
如果是对源码编译后,再执行,
Go
的执行流程如下图:
如果是对源码直接 执行
go run
源码,Go 的执行流程如下图:
两种执行流程的方式区别:
- 如果先编译生成了可执行文件
- 那么可以将该可执行文件拷贝到没有 go 开发环境的机器上,仍然可以运行。
- 如果是直接
go run go
源代码
- 那么如果要在另外一个机器上这么运行,也需要
go
开发环境,否则无法执行。- 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中
- 所以,可执行文件变大了很多。
开发注意事项
Go 源文件以
go
为扩展名。Go 应用程序的执行入口是
main()
函数。Go 语言严格区分大小写。
Go 方法由一条条语句构成,每个语句后不需要分号(Go 语言会在每行后自动加分号)。
Go 编译器是一行行进行编译的
- 因此一行就写一条语句,不能把多条语句写在同一个,否则报错。
Go 语言定义的变量或者
import
的包如果没有使用到,代码不能编译通过。大括号都是成对出现的,缺一不可。
通过
go run xxx.go
直接运行Go代码。编译后再运行
go build xxx.go
,./xxx
。
标识符
命名规则:
由26个英文字母大小写,0-9,_ 组成
数字不可以开头:
var num int
//这样写是OK的vat 3num int
//这样是错误的Golang中严格区分大小写
标识符不能包含空格
下划线
_
本身在Go中是一个特殊的标识符,称为空标识符
- 可以代表任何其他的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)
- 所以仅能被作为占位符使用,不能作为标识符使用
不能以系统保留关键字作为标识符(一共25个),比如
break , if
等等命名注意事项
- 包名:保持和package的名字和目录是一样的,尽量采取用意义的包名,简短,有意义
- 不要和标准库有冲突,例如 fmt
- 变量名、函数名、常量名:采用驼峰法
- 如果变量名、函数名、常量名首字母大写,则可以被其他包访问:如果首字母小写,则只能在本包中使用
转义字符
常用的转义字符有如下:
\t
: 表示一个制表符,通常使用它可以排版\n
:换行符\\
:一个\\"
:一个”\r
:一个回车
1 | package main |
注释
行注释:
- // 注释内容
块注释(多行注释):
/*注释内容*/
变量
声明变量的规则:
- 以字母或下划线开头,由一个或多个字母、数字、下划线组成
关键字:var
定义变量:
- 注意Go语言变量一旦定义后续必须要用到,否则会报错
定义变量格式:
- var 变量名 变量类型
1 | var a int |
变量赋值:
- var 变量名 变量类型 = 值
- 根据值自行判定变量类型
- 在函数内第一次赋值时可以使用:=替代var来定义赋值变量(在函数外不可以)
- 编译器也可以自动决定类型
- 在函数外定义变量是包内变量(不是全局变量)
- 如果变量已经使用 var 声明过了,再使用
:=
声明变量,就产生编译错误
1 | var a int = 5 |
变量赋值与其他主流语言的区别:
- 赋值可以进行自动类型推断
- 在一个赋值语句中可以对多个变量进行同时赋值
1 | func TestExchange(t *testing.T) { |
程序中+号的使用
当左右两边都是数值型时,则做加法运算
当左右两边有一方为字符串,则做拼接运算
全局变量:
- 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效
- 在函数中可以访问到全局变量。
1 | package main |
常量
关键字
const
:
- const可以作为各种类型进行使用
1 | const a,b = 3,4 |
变量赋值与其它主流语言的区别:
- Go语言可以快速设置连续值
1 | const ( |
数据类型
布尔型
- 布尔型的值只可以是true或者false
数字类型
- 整型int、浮点型
float32,float64
GO语言支持整型和浮点型数字
- 并且支持复数,其中位的运算采用补码
字符串类型
- 字符串就是一串固定长度的字符连接起来的字符序列。
- Go 的字符串是由单个字节连接起来的。
- Go 语言的字符串的字节使用 UTF-8 编码标识
Unicode
文本。派生类型
- 指针类型(Pointer)
- 数组类型
- 结构化类型(struct)
- Channel 类型
- 函数类型
- 切片类型
- 接口类型(interface)
- Map 类型
零值(初始值)
- 当一个变量或者新值被创建时,如果没有为其明确指定初始值。
- Go语言会自动初始化其值为此类型对应的零值,各类型零值如下:
- bool:false
- integer:0
- float:0.0
- string:
""
pointer, function, interface, slice, channel, map:nil
- 对于复合类型,Go语言会自动递归地将每一个元素初始化为其类型对应的零值:
- 比如:数组,结构体。
与其他主流编程语言的差异
- Go语言不允许隐式类型转换
- 别名和原有类型也不能进行隐式类型转换
string
是值类型:默认初始化值是空字符串,不是nil
- 不支持指针运算
数组
数组定义
在Go语言中,数组从声明时就确定,使用时可以修改数组成员
- 但是数组大小不可变化。
1 | var 数组变量名 [元素数量]T |
比如:
var a [5]int
, 数组的长度必须是常量
- 并且长度是数组类型的一部分。
一旦定义,长度不能变。
[5]int
和[10]int
是不同的类型。
1 | var a [3]int |
数组的初始化
初始化数组时可以使用初始化列表来设置数组元素的值。
1 | func main() { |
按照上面的方法每次都要确保提供的初始值和数组长度一致
- 一般情况下可以让编译器根据初始值的个数自行推断数组的长度。
1 | func main() { |
还可以使用指定索引值的方式来初始化数组。
1 | func main() { |
数组的遍历
1 | func main() { |
多维数组
二维数组的定义:
1 | func main() { |
二维数组的遍历:
1 | func main() { |
数组是值类型
数组是值类型,赋值和传参会复制整个数组
- 因此改变副本的值,不会改变本身的值。
1 | func modifyArray1(x [3]int){ |
切片
数组的局限:数组的长度是固定的,并且长度属于类型的一部分。
切片可以理解为长度可以动态变化的数组
- 它是基于数组类型做的一层封装,非常灵活,支持自动扩容。
切片(
Slice
)是一个拥有相同类型元素的可变长度的序列。切片是一个引用类型,它的内部结构包含
地址
、长度
和容量
。
- 切片一般用于快速地操作一块数据集合。
切片中可以容纳元素的个数称为容量,容量大于等于长度
- 可以通过
len(slice)
和cap(clice)
分别获取切片的长度和容量。可以通过
make(type,len,cap)
的方式创建出自动以初始长度和容量的切片
- 在追加元素的过程中,如果容量不够用时,就存在动态扩容问题
- 动态扩容采用的是倍增策略,即:
新容量=2*就容量
。扩容后的切片会得到一片新的连续内存地址,所有元素的地址都会随之发生改变。
切片的定义
1 | var name []T |
切片表达式
切片表达式从字符串、数组、指向数组或切片的指针构造字符串或切片,有两种变体:
- 一种指定start和end两个索引界限值得简单形式
- 另一种是除了start和end索引界限值外还制定容量的完整形式。
简单切片表达式:
切片的底层就是一个数组,可以基于数组通过切片表达式得到切片
- 切片中的
start
和end
表示一个索引范围(左包含,右不包含)
1 | func main(){ |
为了方便,通常可以省略切片表达式中的任何索引
- 省略了
start
则默认是0,省略了end
则默认到结尾。
1 | a[2:] // a[2:len(a)] |
完整切片表达式:
对于数组,指向数组的指针,或切片(注意不能是字符串)支持完整切片表达式。
1 | a[start:end:max] |
上面的代码会构造与简单切片表达式
a[start:end]
相同类型,相同长度和元素的切片
- 另外,它会将得到的结果切片的容量设置为
max-start
。在完整切片表达式中只有第一个索引值(start)可以省略,默认为0。
1 | func main(){ |
完整切片表达式需要满足的条件是
0<=start<=end<=max<=cap(a)
- 其他条件和简单切片表达式相同。
使用make()函数构造切片
上面都是基于数组来创建的切片
- 如果需要动态的创建哪一个切片,就需要使用内置函数
make()
1 | make([]T,size,cap) |
1 | func main(){ |
a
的内部存储空间已经分配了10个,但是实际上只用了2个,容量并不会影响当前元素的个数
- 所以
len(a)
返回2,cap(a)
则返回该切片的容量。
切片的本质
切片的本质就是对底层数组的封装,包含三个信息:
- 底层数组的指针,切片的长度和切片的容量。
现在有一个数组
a:=[8]int{0,1,2,3,4,5,6,7}
,切片s1:=a[:5]
相应的示意图如下:
切片
s2:=a[3:6]
,相应的示意图如下:
切片的赋值拷贝
拷贝前后两个变量共享底层数组
- 对一个切片的修改会影响另一个切片的内容。
1 | func main(){ |
切片的遍历
切片的遍历方式和数组是一致的,支持索引遍历和
for range
遍历。
1 | func main(){ |
向切片中添加元素
Go语言的内建函数
append()
可以为切片动态添加元素
- 可以一次添加一个元素,可以添加多个元素,也可以添加另外一个切片中的元素
1 | func main(){ |
通过var声明的零值切片可以在
append()
函数直接使用无需初始化。没有必要初始化一个切片再传入
append()
函数使用。
1 | s := []int{} // 没有必要初始化 |
从切片中删除元素
Go语言中并没有删除切片元素的专用方法,可以通过使用切片本身的特性来删除元素。
1 | func main(){ |
从切片a中删除索引为
index
的元素
- 操作方法是
a = append(a[:index], a[index+1:]...)
函数
函数的声明:
函数的声明:
- 使用 func 关键字,后面依次接
函数名
,参数列表
,返回值列表
,用 {} 包裹的代码逻辑体
- 形式参数列表描述了函数的参数名以及参数类型
- 这些参数作为局部变量,其值由参数调用者提供
- 返回值列表描述了函数返回值的变量名以及类型
- 如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的
1 | func 函数名(形式参数列表)(返回值列表){ |
函数名:由字母、数字、下划线组成。
- 但函数名的第一个字母不能是数字:函数名也不能重复命名。
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分割返回值:返回值由返回值变量和其变量类型组成
- 也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分割
举个例子,定义一个 sum 函数,接收两个 int 类型的参数
- 在运行中,将其值分别赋值给 a,b,并规定必须返回一个int类型的值
1 | func sum(a int, b int) (int){ |
无参数无返回值函数
函数可以有参数也可以没有参数,可以有返回值也可以没有返回值
1 | func main() { |
可变参数函数
Go语言支持可变参数函数
可变参数指调用参数时,参数的个数可以是任意个
可变参数必须在参数列表最后的位置
- 在参数名和类型之间添加三个点表示可变参数函数
1 | func 函数(参数,参数,名称 ... 类型 ){ |
多返回值
函数如果有多个返回值时必须用
()
将所有返回值包裹起来。
匿名函数
匿名函数就是没有名称的函数
正常函数可以通过名称多次调用,而匿名函数由于没有函数名
- 所以大部分情况都是在当前位置声明并立即调用(函数变量除外)
匿名函数声明完需要调用,在函数结束大括号后面紧跟小括号
- 匿名函数都是声明在其他函数内部
1 | func (){ |
函数作为参数或返回值
变量可以作为函数的参数或返回值类型,而函数既然可以当做变量看待
- 函数变量也可以当做函数的参数或返回值
函数作为参数时,类型写成对应的类型即可
1 | func main() { |
Defer语句
Go语言中的
defer
语句会将其后面跟随的语句进行延迟处理。在
defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行
- 也就是说,先被
defer
的语句最后被执行,最后被defer
的语句,最先被执行。由于
defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。
- 比如:资源清理、文件关闭、解锁及记录时间等。
1 | func main() { |
defer执行时机:
- 在Go语言的函数中
return
语句在底层并不是原子操作
- 它分为给返回值赋值和RET指令两步。
- 而
defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。
全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。
在函数中可以访问到全局变量。
1 | package main |
结构体
Go语言中的基本数据类型可以表示一些事物的基本属性
- 但是当想表达一个事物的全部或者部分属性时
- 这时候再用单一的基本数据类型明显就无法满足需求。
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型
- 这种数据类型叫结构体,
struct
。Go语言中通过
struct
来实现面向对象。
结构体的定义
使用
type
和struct
关键字来定义结构体。
1 | type 类型名 struct{ |
1 | type person struct{ // 定义一个person的结构体 |
同类型的字段也可以写在一行。
1 | type person struct{ |
结构体实例化
只有当结构体实例化时,才会真正地分配内存,也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型
- 可以像声明内置类型一样使用
var
关键字来声明结构体类型。通过
.
来访问结构体字段(成员变量),例如p.name
和p.age
等。
1 | var 结构体实例 结构体类型 |
匿名结构体
在定义一些临时数据结构等常见下可以使用匿名结构体。
1 | package main |
创建指针类型结构体
通过使用
new
关键字对结构体进行实例化,得到的是结构体地址。
1 | var p = new(person) |
从打印结果来看,此时
p
是一个结构体指针。在Go语言中支持对结构体指针直接使用
.
来访问结构体成员。
1 | var p = new(person) |
取结构体的地址实例化
使用
&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
1 | p := &person{} |
p.name="Negan"
其实在底层是(*p3).name="Negan"
- 这是Go语言实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
1 | type person struct{ |
使用键值对初始化
使用键值对对结构体进行初始化,键对应结构体的字段,值对应该字段的初始值。
1 | p := person{ |
也可以使用结构体指针进行键值对初始化。
1 | p := &person{ |
当某些字段没有初始值的时候,该字段可以不写
- 此时没有指定初始值的字段的值就是该字段类型的零值。
1 | p := &person{ |
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段
- 初始值的填充循序必须与字段在结构体中的声明顺序一致
- 该方式不能和键值初始化方式混用
1 | p := &person{ |
结构体的继承
Go语言中使用结构体可以实现其他编程语言中的面向对象继承。
1 | // Animal 动物 |
结构体字段的可见性
结构体中字段大写开头表示公开访问
- 小写表示私有(仅在定义当前结构体的包中可访问)。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或者结构体指针。
1 | // Address 地址结构体 |
嵌套匿名结构体
当访问结构体成员时会现在结构体中查找该字段,找不到再去匿名结构体中查找。
1 | // Address 地址结构体 |
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名
- 这个时候为了避免歧义需要制定具体的内嵌结构体的字段。
1 | // Address 地址结构体 |
指针
指针是一个地址,指针类型是依托于某一个类型而存在的。
Go
里面的基本数据类型int
、float64
、string
等
- 它们所对应的指针类型为
*int
、*float64
、*string
等。指针的定义:
- 语法格式:
var 指针变量名 *数据类型 = &变量
。&
为取地址符号,通过&
符号获取某个变量的地址,然后赋值给指针变量。
1 | import ( |
npmPtr
指针变量指向变量num
,0xc00001c098
为num
变量的地址
0xc00000a028
为指针变量本身的地址值。
使用
new(T)
函数创建指针变量:
new(T)
函数为每个类型分配一片内存
- 内存单元保存的是对应类型的零值,函数返回一个指针类型。
1 | import ( |
错误的类型地址赋值:
- 当指针所依托的类型与变量的类型不一致时,Go 编译器会报错,类型不匹配。
1 | func main() { |
获取和修改指针所指向变量的值
- 通过指针获取所指向变量的值
- 对指针使用
*
操作符可以获取所指向变量的值。
1 | func main() { |
通过指针修改所指向变量的值
- 同时也可以对指针使用
*
操作符修改所指向变量的值。
1 | import ( |
值类型和引用类型
值类型:
- 基本数据类型int系列,float系列,bool,string,数组和结构体 struct
引用类型:
- 指针,slice切片,map,管道chan,interface等都是引用数据类型
值类型:
- 变量直接存储值,内存通常在栈中分配
引用类型:
变量存储的是一个地址,这个地址对于的空间才是真正的存储数据(值)
- 内存通常在堆上分配,当没有任何变量引用这个地址时
- 改地址对应的数据空间就成为了一个垃圾,由GC来回收
内存的栈区和堆区:
并发
Go语言中的并发通过
goroutine
实现。
goroutine
类似于线程,属于用户态线程
- 可以根据需要创建成千上万个
goroutine
并发工作。
goroutine
是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。Go语言还提供
channel
在多个goroutine
间进行通信。
Goroutine
Go程序会将
goroutine
中的任务合理地分配给每个CPU。当需要让某个任务并发执行的时候
- 只需要把这个任务包装成一个函数,开启一个
goroutine
去执行这个函数就行。一个
goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
启动单个goroutine
使用
goroutine
只需要在调用函数的时候在前面加上go
关键字
- 就可以为一个函数创建一个
goroutine
。
1 | func hello(){ |
在程序启动时,Go程序就会为
main()
函数创建一个默认的goroutine
。当
main()
函数返回的时候该goroutine
就结束了
- 所有在
main()
函数中启动的goroutine
会一同结束。
出让资源
通过
runtime.Gosched()
出让资源,让其他goroutine
优先执行。
1 | func main() { |
自杀
通过
runtime.Goexit()
实现自杀,自杀前会执行提前定义的defer语句
- 同时调用它的
goroutine
也会跟着自杀。
1 | func test(){ |
主
goroutine
结束后,会带走未结束的子goroutine
。同时如果主
goroutine
暴毙,会令所有的子goroutine
失去牵制,等所有的子goroutine
都结束后程序会崩溃:
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
。
启动多个Goroutine
使用
sync.WaitGroup
来实现goroutine
的同步。
1 | var wg sync.WaitGroup |
Channel
channe1
可以让一个goroutine
发送特定值到另一个goroutine
的通信机制。Go语言中的通道(
channel
)是一种特殊类型,通道像一个传送带或者队列
- 总是遵循先进先出的规则,保证数据的收发顺序
每一个通道都是一个具体类型的导管,也即是声明
channel
时候需要为其制定元素类型。
channel类型
channel
是一种类型,一种应用类型,声明通道类型的格式如下:
1 | var 变量 chan 元素类型 |
创建channel
通道是引用类型,通道类型的控制是
nil
1 | var ch chan int |
声明的通道需要使用
make
函数初始化后才能使用创建
channel
的格式如下:
1 | make(chan 元素类型,[缓冲大小]) |
channel操作
通道有读、写和关闭三种操作。
读和写都是用
<-
符号。
1 | // 初始化一个channel |
channel类型
channel
分不带缓冲区的
channel和带缓冲区的
channel。
无缓冲区
无缓冲
channel
从无缓冲的channel
中读取消息会阻塞
- 直到有
goroutine
向该channel
中发送消息。同理,向无缓冲区的
channel
中发送消息也会阻塞
- 直到有
goroutine
从channel
中读取消息。使用无缓冲通道进行通道将导致发送和接收的
goroutine
同步化
- 因此无缓冲通道也被称为
同步通道
。
1 | func recv(c chan int){ |
有缓存的通道
有缓存的
channel
的声明方式为指定make
函数的第二个参数
- 该参数为
channel
缓存的容量。有缓存的
channel
类似于一个阻塞队列(采用环形数组实现)。当缓存未满时,向
channel
中发送消息不会阻塞
- 当缓存满时,发送操作将会阻塞,直到有其他
goroutine
从中读取消息。相应的,当
channel
中消息不为空是,读取消息不会出现阻塞
- 当
channel
为空时,读取操作会发生阻塞,直到有goroutine
向channel
中写入消息。可以通过使用内置的
len()
函数获取通道内元素的数量
- 使用
cap()
函数获取通道的容量。
1 | func main() { |
互斥锁
有时候在Go代码中可能存在多个
goroutine
同时操作一个资源(临界区)
- 这种情况会发生
竟态问题
(数据竟态)。互斥锁是一种常用的控制共享资源访问的方法
- 它能够保证同时只有一个
goroutine
可以访问共享资源。Go语言中使用
sync
包的Mutex
类型来实现互斥锁。
1 | func main() { |
通过信号量控制并发数
控制并发数属于常用的调度,规定并发的任务都必须现在某个监视管道中进行注册
- 而这个监视管道的缓存能力是固定的
- 比如说5,那么注册在该管道中的并发能力也是5。
1 | var sema chan int |
定时器
Go提供了两种方式的计时器:
定时执行任务的计时器和周期性执行任务的计时器
固定时间定时器
1 | func main() { |
上面的示例演示了如何使用定时器延时两秒执行一项任务。
上面的示例也可以写成下面的形式。
1 | func main() { |
提前终止计时器
计时器被中途stop掉了,被延时的
goroutine
将永远得不到执行,
1 | func main() { |
中途重置定时器
下面的例子中,timer在配置为延时10秒执行后
- 又被重置为1秒,所以其时间延时为一秒。
需要注意的是:
如果在reset的一刹那,定时器已经到时或者已被stop掉,则reset是无效的。
1 | func main() { |