如何查看Go官网的文档?
在安装有Go的计算机命令行下使用godoc -http=:6060就可以通过localhost:6060访问Go官方文档。
也可以访问golang.google.cn不需出墙保卫长城。
什么是Go?
一个提供并发支持、垃圾回收的编译型语言,兼有静态编译语言的高性能和动态语言的高效开发(Go的编译速度快)的优势。
Go的主要特性:
- 类型安全(类型简单,有int string等内置类型,也有用户自定义类型,使用组合的方式就能复用类型的所有功能,避免传统面向对象继承模型的繁琐)和内存安全(语言自带垃圾回收器,不需要用户自己管理内存,垃圾回收增加了一些额外的开销但能显著降低开发难度)
- 以非常直观且代价极低的方式实现高并发,goroutine很像线程,但占内存更少,Go的并发模型是让数据在多个goroutine之间通过通道chanel传递,而不是多个goroutine争夺统一数据使用权。
- 高效的垃圾回收机制
- 快速编译
- 为多核计算机提供性能提升方案(能够指定使用多少个核)
- UTF-8编码的支持,避免乱码
Windows下安装Go的步骤?
- 1.下载msi文件,点击安装,安装程序会自动设置好GOROOT(Go安装目录)、GOTOOLDIR(工具目录)等环境变量,但是GOPATH环境变量(系统变量)需要自己手动配置
- GOPATH环境变量是Go程序的开发目录,也就是用户的工作目录,GOPATH下有多个工作目录的话在设置系统变量的时候可以用分号;分隔多个目录。根据约定需要在GOPATH的工作目录下建立三个独立文件夹,分别是:bin(存放编译后的二进制可执行文件)、pkg(用于存放编译好的包文件,文件拓展名为.a)、src(存放go源码文件)
Go语言常用的命令(在命令行或终端输入go可查看所有go命令)
go get: 获取远程包,需要提前安装git或者hg(从google code上获取远程包时使用)
go run:直接运行程序
go build:测试编译,检查是否有编译错误,编译当前目录下package为main的文件时会生成对应的可执行文件。package不是main的源码文件是包的源码文件,使用go build不会生成可执行文件。
go install: 先编译包文件后编译整个程序(主程序),包编译后的文件放到pkg目录下,编译得到的可执行文件放到bin目录下。 如果编译到bin目录下记得把可执行文件拷贝到项目文件根目录下,因为在项目各文件中我们一般使用相对路径,为了避免执行错误要把.exe文件拷贝回来
go fmt:格式化源码,所有的go源码都需保存为相同格式,保证格式的统一,一些IDE保存时会自动调用该命令
go doc:查看文档,多用godoc在浏览器中查看
go test:运行测试文件(运行当前目录下已_test.go结尾的文件)
cmd | meaning | |
---|---|---|
build | compile packages and dependencies | |
clean | remove object files | |
doc | show documentation for package or symbol | |
env | print Go environment information | |
bug | start a bug report | |
fix | run go tool fix on packages | |
fmt | run gofmt on package sources | |
generate | generate Go files by processing source | |
get | download and install packages and dependencies | |
install | compile and install packages and dependencies | |
list | list packages | |
run | compile and run Go program | |
test | test packages | |
tool | run specified go tool | |
version | print Go version | |
vet | run go tool vet on packages |
Go编程基础
Go内置的25个关键字(均为小写):
break | default | func | interface | select | |
case | defer | go | map | struct | |
chan | else | goto | package | switch | |
const | fallthrough | if | range | type | |
continue | for | import | return | var |
Go的注释方法与C++一致
Go程序的一般结构:
- Go程序通过package来组织(与Python一致)
- 只有package名称为main的包能包含main函数
- 一个可执行程序有且只有一个main包
- 通过关键字import来导入其他非main的包
- 导入的包必须使用,否则编译不通过
- 通过关键字const来定义常量
- 通过在函数体外部使用var关键字来声明和赋值全局变量
- 通过type关键字来声明结构struct或者接口interface
- 通过func关键字来声明函数
1 | // 当前程序的包名 |
可见性规则:Go语言中,使用大小写来决定常量、变量、类型、接口、结构或者函数是否可以被外部包所调用,首字母小写即为private,首字母大写即为public
Go的基本类型:
rune、byte这些别名或者类型存在的原因,就是从类型定义层面就增加代码的可阅读性,rune类型一般就是用来处理Unicode相关的类型,而byte用来处理字节相关的类型
- 布尔型:bool -长度:1字节; -取值范围:true false; -注意事项:不可以用数字代表true或false
- 整型:int/uint -根据平台可能为32位或者64位
- 8位整型:int8/uint8 -长度:1字节 -取值范围:-128~127/0~255
- 16位整型:int16/uint16 -长度:2字节 -取值范围:-32768~32767/0~65535
- 32位整型:int32(rune)/uint32 -长度:4字节 -取值范围:-2^32/2~2^32/2-1或者/0~2^32-1
- 64位整型:int64/uint64 -长度:8字节 -取值范围:-2^64/2~2^64/2-1或者/0~2^64-1
- 字节型:byte(uint8别名)在Go中如果要使用C语言中的char类型,一般使用byte类型
- 浮点型:float32/float64 -长度:4/8字节 -小数位:精确到7/15小数位 (由于有32位和64位浮点型之分,就可以省去double类型)
- 复数:complex64/complex128 -长度:8/16字节
- 足够保存指针的32位或者64位(根据操作系统平台的类型决定)整型:uintpr
- 其他类型:array struct string
- 引用类型:slice map chan
- 接口类型:interface
- 函数类型:func 在Go语言中,函数是可以赋值给变量的,所以函数也是一种类型
类型的零值:类型的零值不等于空值,而是当变量被声明为某种类型之后的默认值,通常情况下值类型的默认值为0,bool为false,string为空字符串
math包中的math.maxInt32等可以查看类型的数值范围
类型的别名:
1 | type { |
Go中变量的声明与赋值:
单个变量的声明与赋值:
- 变量声明的格式: var <变量名称> <变量类型>
- 变量的赋值格式:<变量名称>=<表达式>
- 变量声明的同时赋值: var <变量名称> <变量类型> = <表达式>
1
2
3
4
5
6
7
8
9
10
11 var name string // 变量的声明
name="peter" // 变量的赋值
// 变量声明的同时赋值
var id int = 9527
// 声明变量并赋值时省略类型,类型由系统推断
var cardId = 6277
// 变量声明与赋值的最简写法,var关键字和类型都省略了,由系统推断
d := 456 // :代表了var关键字
多个变量的声明与赋值:
- 全局变量的声明可以使用var()组的方式简写
- 全局变量的声明不可以省略var,故不能使用:=的形式,但可以使用并行方式
- 所有变量都可以使用类型推断
- 局部变量不可以使用var()的方式简写,只能使用并行方式
- :=使用最广泛的是在获取函数返回值的时候
- 可以使用下划线对某一值进行忽略(是作为blank identifier空标识符)
1 | var{ |
变量的类型转换:
- Go中不存在隐式转换,所有类型类型转换必须显式声明 (保证了Go的类型安全特性)
- 转换只能发生在两种相互兼容的类型之间
- 类型转换的格式:
[:]= [:]表示:可以省略,根据valueA是否需要声明来决定是否省略( )
1 | // 在相互兼容的两种类型之间进行转换 |
解释下面程序的结果:
1 | func main() { |
Go中常量的声明与赋值:
常量的定义:
- 常量的值在编译的时候就已经确定
- 常量的定义格式与变量基本相同
- 等号右侧必须是常量或者常量表达式(因为常量的值在编译时就确定,在运行时才确定的表达式的值就不满足要求,会编译报错)
- 常量表达式中的函数必须是内置函数(因为只有内置函数在编译时才是确定的)
1 | // 定义单个常量 |
常量组初始化规则,在定义常量组时,如果不提供初始值,则表示将使用上行的表达式:1
2
3
4
5
6
7
8
9
10const (
a = 1
b = 'e'
c
)
func main() {
fmt.Println(c) // 输出结果101,是'e'的ASCII码值
// 即当常量组里有没有初始化的常量时,则编译器使用上一行的表达式来对它进行初始化,即执行c='e'
}
1 | const ( |
1 | const ( |
定义常量时等号右侧必须是常量或者常量表达式的经典例子:
1 | package main |
1 | package main |
常量初始化规则与枚举
- 在定义常量组时,如果不提供初始值,则使用上一行的表达式对其赋值
- 使用相同的表达式不代表具有相同的值
- iota是常量计数器,从0开始,组中每定义1个常量自动递增1
- 通过初始化规则与iota可以达到枚举的效果
- 每遇到一个const关键字,iota就会重置为0
1 | const { |
1 | // 星期枚举 |
Go中的运算符:
Go中的运算符都是从左向右结合
优先级从高到低依次为:1
2
3
4
5
6
7- ^(^x一元运算符,bitwise complement or bitwise not按位取反) !
- * / % << >> &^(bit clear (AND NOT)将右边操作数先按位取反^,再和左边操作数进行按位与&运算)
- | ^(二元运算符XOR)
- == != < <= >= >
- <- (专门用于channel)
- &&
- ||
思考:请尝试结合常量的iota与<<运算符实现计算机存储单位的枚举?
1 | type ByteSize float64 |
Go中的指针:
Go保留了指针,与其他编程语言例如C语言不通的是,在Go中不支持指针运算以及->运算符,而直接采用.运算符来操作指针对象的成员
- 操作符&取变量的地址,使用*通过指针间接访问目标对象
- 指针默认值为nil而不是null
1 | func main() { |
Go中,++ –是作为语句而不是作为表达式,两者的区别是表达式是可以放在等号右边的,而语句是不可以放在等号右边的,只能作为单独的语句:
1 | func main() { |
Go中的控制语句:
if控制语句:
- 条件表达式没有括号
- 支持一个初始化表达式(可以采用并行方式初始化多个变量)
- Go中if控制语句的左大括号必须和条件语句或者else同一行 (严格定义了代码书写的规范)
- 支持单行模式
- 初始化语句中的变量为block级别,同时隐藏外部同名变量
1 | func main() { |
for循环语句:
- Go语言中只有一个for循环语句关键字,但支持三种形式,简洁高效
- 初始化和步进表达式可以是多个值
- 条件语句每次循环都会被重新检查,因此不建议在条件语句中使用函数,尽量提前计算好条件并以变量或者常量代替,避免在循环中频繁调用函数导致程序性能骤降
- 左大括号必须和条件语句在同一行
1 | // 无限循环形式 |
1 | // 类似while循环结构的形式 |
1 | // for经典形式 |
选择语句switch:
- 可以使用任何类型或者表达式作为条件语句
- 不需要写break,一旦条件符合自动终止
- 如希望继续执行下一个case,需使用fallthrough语句
- 支持一个初始化表达式(可以是并行方式),右侧需要跟分号
- 左大括号必须和条件语句在同一行
1 | func main() { |
1 | // Go中的switch语句switch后的条件可以省略,case后面不仅仅可以写常量,也可以写表达式 |
1 | func main() { |
1 | func main() { |
Go语言中的跳转语句goto,break,continue:
- 三个都可以配合标签使用
- 标签名区分大小写,若不使用会造成编译错误
- break与continue配合标签可以用于多层循环的跳出,跳出的循环是与标签同级的循环
- goto是调整执行位置,与其它2个语句配合标签的结果并不相同
1 | func main() { |
Go中的数组Array:
- 定义数组的格式: var
[n] , n>=0 - 数组长度也是类型的一部分,因此具有不同长度的数组是不同的类型
- 注意区分指向数组的指针和指针数组
- 数组在Go中为值类型,其它语言多将数组作为引用类型,值类型的意思是在Go中将数组作为函数的参数时,作为参数的数组会被拷贝,而不是直接引用这个数组(将地址传递给方法和函数)。数组的引用传递可以使用slice切片
- 数组之间可以使用==或者!=进行比较,但不能使用>和<。注意:长度不同的数组由于是不同的类型,故不可以使用==或者!=来进行比较,若使用会编译报错 元素的类型不同的数组当然也不能直接进行比较
- 可以使用new来创建数组,此方法返回一个指向数组的指针
- Go支持多维数组
1 | func main() { |
1 | func main() { |
1 | func main() { |
1 | // 数组指针与指针数组 |
1 | func main() { |
1 | // 无论是数组本身,还是指向数组的指针,都可以使用[下标(索引)]的形式对数组中的单个元素进行操作 |
1 | // Go中的多维数组的定义,形式类比c |
1 | // 小例子:数组的排序 |
Go中的slice切片:
- 本身并不是数组,它指向一个底层的数组
- slice是作为变长数组的替代方案(array是不可变长的),slice可以关联底层数组的局部或者全部
- 由于slice指向底层的数组,也就是slice是指针,故slice是引用类型
- slice可以直接创建或者从底层数组获取生成
- 使用len()获取元素个数(同array),使用cap()获取容量
- 一般使用make()函数创建slice,不使用new,因为new得到的是一个指针,而slice本身又是一个指针,用new就会得到指向指针的指针
- 如果多个slice指向相同底层数组,其中一个的值发生改变会影响全部
- make([]T, len, cap)
- 其中cap可以省略,省略则默认cap和len的值相同
- len表示存数的元素个数,cap表示容量
1 | // slice的声明 |
关于reslice:
- reslice时索引以被slice的切片为准,不再以原slice指向的数组为准
- 索引不可以超过被slice的切片的容量值
- 索引越界不会导致底层数组的重新分配,而是会导致错误
append函数:
- 可以在slice尾部追加元素
- 可以将一个slice追加在另一个slice尾部,相当于数组拼接的效果
- 如果最终长度未超过追加到的slice的容量则返回原始slice,不会重新分配数组空间
- 如果超过追加到的slice的容量则将重新分配数组并拷贝原始数据
1 | func main() { |
1 | func main() { |
1 | func main() { |
copy函数:
1 | func main() { |
Go中的map:
- 类似其他语言中的哈希表或者字典,以key-value形式存储数据
- key必须是支持==或者!=比较运算符的类型,不可是函数、map或者slice,int、string、byte类型的数据都是可以使用比较运算符的
- Map查找比线性搜索快得多,但比使用索引访问数据慢100倍
- Map使用make()创建,支持:=的简写创建方式
- make(map[keyType]valueType,cap),cap表示容量,可以省略
- 超出容量时会自动扩容,但尽量使用一个合理的初始值
- 使用len()函数获取元素个数
- 访问的键不存在时会自动添加该键,如类型为[int]string的map1中没有键1时执行代码map1[1]=hello,解释器会自动添加键1值为hello的键值对
- 使用delete()可以删除键值对
- 使用for range对map和slice进行迭代操作,类似于其他语言的for each,但是更加高级
1 | func main() { |
1 | // 复杂map的操作 |
1 | import ( |
1 | func main() { |
1 | // slice的元素为map,如何进行初始化 |
Go中的函数function
- Go语言中的函数不支持嵌套、重载和默认参数
- Go中的函数支持以下特性:无需声明原型(C语言中存在函数之间的复杂调用要先声明一下函数的原型再使用函数)、不定长度变参、多返回值、命名返回值参数、匿名函数和闭包
- Go语言中的匿名函数不能作为顶级函数(最外层函数),只能作为内部函数存在其他函数中
- 定义函数使用关键字func,且左大括号不能另起一行
- 函数也可以作为一种类型使用
ps:什么是闭包closure-
一般程序中局部变量都会有一个作用范围scope,闭包就是持续存在的局部变量范围(A closure is a persistent local variable scope),让局部变量能够在正常的scope之外还能存在。那这个范围是多大呢?与绑定的函数的作用范围一致(The scope object, and all its local variables, are tied to the function, and will persist as long as that function persists.)。
1 | // 没有参数和返回值的函数 |
1 | // Go中的闭包 |
1 | // 附一段js的闭包例子 |
Go中的defer关键字
- defer的执行方式类似其他语言中的析构函数,在函数体执行结束后按照调用顺序的相反顺序逐个执行
- 即使函数发生严重错误也会执行
- 支持匿名函数的调用
- 常用于资源清理、文件关闭、解锁以及记录时间等操作,先定义的defer函数会最后执行,后定义的defer函数会最先执行
- 通过与匿名函数配合可在return之后修改函数计算结果
- 如果函数体内某个变量作为defer时匿名函数的参数,则在定义defer时即已经获得了拷贝,否则则是引用某个变量的地址 类似闭包
- Go没有异常机制(没有try
catch捕获机制,但有类似finally最终无论如何都要执行的语句),Go中使用panic()/recover()模式来处理错误。具体做法是利用Go中多返回值的特性来得到error类型的返回,在调用这个函数之后进行check(借助defer,因为程序发生panic之后正常的函数都会停止执行,只有defer不管程序是否发生错误它都会执行),check一下error是否等于nil,不为nil就是发生了错误,以此来判断函数在运行过程中是否发生了错误。- 含有recover的defer函数必须要在recover对应处理的panic之前注册,否则该recover无效
- Panic可以在任何地方引发,但recover只有在defer调用的函数中有效
1 | func main() { |
1 | func main() { |
1 | func main() { |
1 | func main() { |
Go中的结构struct:
- Go中的struct和C中的struct非常相似,而且Go没有class,结构承担了Go中面向对象的功能
- 使用type
struct{}的方式定义结构,名称遵循可见性原则(大小写控制public或者private) - 支持指向自身的指针成员,可以使用类似C语言中指针的形式来定义指向结构的指针,并且无论是指向结构的指针还是结构变量本身都可以使用.访问结构中的成员
- 支持匿名结构,可用作成员或定义成员变量。既可以有匿名成员变量(此时初始化的顺序要严格按照定义的顺序),也可以有匿名结构作为结构的成员(初始化一定要使用.的形式,不能用{}直接初始化的形式,因为无法得知匿名结构的名称)
- 匿名结构也可以用作map的值
- 可以使用字面值对结构进行初始化,也就是使用{}中加上值的形式直接进行初始化,不需要另外加上语句使用.来进行初始化
- 允许直接通过指针来读写结构体成员(同样的也是直接使用.,不像C中使用->,因为Go中没有指针运算,Go进行了优化可以直接使用.)
- 相同类型的成员可以直接进行赋值拷贝
- 支持==和!=比较运算符,但不支持<或>
- 支持匿名字段,本质上是定义了以某个类型名为名称的字段
- 嵌入结构作为匿名字段看上去像是继承但不是继承,Go中没有继承,使用组合模式作为替代
- 可以使用匿名字段指针
Go中的method:
- Go中没有class,但是依然有method,写method的形式和写函数类似,就是多了一个receiver接收者的概念,编译器根据receiver来判断该方法属于哪个结构struct
- 虽说Go中没有方法重载的概念,但是由于每一个方法都有绑定的receiver,所以不同receiver可以有相同名字的方法,这样就实现了类似重载的功能
- 通过显示说明receiver来实现与某个类型的组合
- 一个结构中的私有字段(首字母小写)可以被其绑定的方法访问,可见方法拥有较高的访问权限。注意Go中的公有私有是针对package而言的,在同一个package中,私有字段的访问级别相当于公有字段,也就是整个包中都是可见的,对于其他包而言,另一个包中的私有字段就是不可见的了
- 只能为同一个包中的类型定义方法
- Receiver可以是类型的值或者指针
- 不存在方法重载
- 可以使用值或者指针来调用方法,编译器会自动完成转换
- 从某种意义上来说,方法是函数的语法糖,因为receiver其实就是方法所接受的第一个参数(method value vs. method expression)
- 如果从外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
- 类型别名不会拥有底层类型所附带的方法
- 方法可以调用结构中非公开的字段
1 | type A struct { |
1 | // 一般的类型也可以绑定方法 |
1 | // 两种不同的形式来调用类型绑定的方法 |
Go中的interface:
- 接口是一个或者多个方法签名的集合
- 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显式声明实现了哪个接口,这称为structural typing
- 接口只有方法声明,没有实现,没有数据字段
- 接口可以匿名嵌入其他接口,或者嵌入到结构中
- 将对象赋值给接口变量时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
- 只有当接口存储的类型和对象都为nil时,借口才等于nil
- 接口调用不会做receiver的自动转换,结构调用的时候变量本身和指针都可以自动转换
- 接口同样支持匿名字段方法
- 接口也可以实现类似OOP中的多态
- 空接口可以作为任何类型数据的容器
- 通过类型的ok-pattern形式可以判断接口中的数据类型
- 使用type switch则可针对空接口进行比较全面的类型判断
- 接口可以转换,可以将拥有超集的接口转换成子集的接口
1 | type USB interface { |
1 | // 接口的嵌入 |
1 | type USB interface { |
1 | // 使用空接口传入任何类型(除了接口类型之外,内置类型也可以),然后使用type switch的形式判断具体是哪个类型 |
Go中的反射:
- 反射可大大提高程序的灵活性,使得interface{}空接口有更大的发挥余地
- 反射使用TypeOf和ValueOf函数从接口中获取目标对象信息
- 反射会将匿名字段作为独立字段(匿名字段本质)
- 想要利用发射修改对象状态,前提是interface.data是settable,即pointer-interface
- 通过反射可以“动态”调用方法
1 | import ( |
1 | type User struct { |
1 | // 使用反射获取嵌套的结构中的字段信息 |
1 | // 使用反射修改值 |
1 | // 使用反射修改目标对象 |
1 | // 使用反射调用方法 效果就像是方法的动态调用 |