简介
分类
- 静态强类型、编译型、并发型
特色
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
用途
- 搭载 Web 服务器
- 存储集群
- 巨型中央服务器的系统
写代码
一个例子
1 | package main // 定义包名,main包表示一个可独立执行的程序,每个Go应用都包含一个名为main的包 |
- 行分割,每个语句是一行,无需使用
;分割
变量
与其他语言有相同之处,也有不同之处
- 基本类型:
int、bool、float32、float64、string(字符串的字节使用 UTF-8 编码标识 Unicode 文本) 衍生类型
指针类型(Pointer)
数组类型
- 比较
1
2
3
4
5
6
7
8slice1 := []int{1,2,3}
slice2 := []int{1,2,3,4}
slice3 := []int{1,2,3}
slice4 := []int{1,3,2}
fmt.Println(slice1 == slice2) // 编译错误,长度不相等,无法比较
fmt.Println(slice1 == slice3) // true
fmt.Println(slice1 == slice4) // false
结构化类型(struct)
Channel 类型
函数类型
切片类型
接口类型(interface)
Map 类型
- 单变量声明方法
var agePtr *int没有初始化则默认为零值var d = true赋初值省略变脸类型f := "Runoob"省略var和变量类型,只能在函数中出现,且只能用于声明
多变量声明
1
2
3
4
5var x, y int
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)所有声明的变量必须被使用,否则会编译失败,因此需要声明但是不会用到的变量可使用
_空白标识符占位相同类型变量的交换赋值可以使用
a, b = b, a常量声明。其中iota为特殊常量,可以认为是一个可以被编译器修改的常量,其值是常量所在的行index
1
2
3
4
5
6
7
8
9
10
11const (
d = 4 // 4
e // 未赋值重复上一个常量赋值,4
)
const (
a = 2 // 2
b = iota //1
c // 2
d = iota * iota // 3 * 3 = 9
e // 4 * 4 = 16
)
运算
- 常规运算:
- 算数运算
+ - * / % ++ -- - 关系运算
> < == != >= <= - 逻辑运算
&& || ! - 位运算
& | ^ >> << &^(按位清零) - 赋值运算
=(以及运算后赋值+=等等) - 其他运算
& *
- 算数运算
条件语句
if和if-elseswitch-case- 不用类似
break语法进行单case控制,可以使用fallthrough使得执行当前case后强行进入下一个case且不用判断下一个case是否为真 - 可以使用
switch …{ case …: }的写法也可以使用switch { case…: }判定”true case”的写法
- 不用类似
select-case- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行,其他被忽略。
- 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。 否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
- 结合
for循环和goroutine可以完成并行
- 不支持
? :形式的条件判断
循环语句
表示方法
for init; condition; post { }for condition { }//等价于C语言中的whilefor { }// while true
or 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环
1
2
3for key, value := range oldMap {
newMap[key] = value
}
函数
1 | func function_name( [parameter list] ) [return_types] { |
变量作用域
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
指针
- 声明方法
var ip *int - 访问指针值
fmt.Printf("*ip 变量的值: %d**\n**", *ip ) - 指针不赋初值默认为
nil空指针
结构体
声明方法
1
2
3
4
5
6type Books struct {
title string
author string
subject string
book_id int
}定义结构体
1
2
3
4
5var Book1 Books // 定义空结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}) // 按顺序定义变量值
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407}) // 指定成员值访问成员
Book1.title = "some title"fmt.Println(Book1.title)
切片
切片是对数组的抽象,可以理解为动态数组,可以追加元素,在追加时可能使切片的容量增大
声明方法
1
2
3var slice1 = []int{1,2,3} // 不指定数组大小声明的数组会被认为是切片
slice := make([]int, 2, 3) //声明一个容量为3,当前内含2个元素的切片
var slice2 = []int // slice2 == nil初始化方法
1
slice2 := slice1[1:] // 通过slice1的引用初始化slice2
append 方法
1
2
3
4
5
6
7
8slice3 := make([]int, 2, 5)
fmt.Println(slice3) // [0, 0]
slice3 = append(slice3, 1)
slice3 = append(slice3, 1)
slice3 = append(slice3, 1)
slice3 = append(slice3, 1) // 自动扩容到10
fmt.Println(slice3) // [0,0,1,1,1,1]
fmt.Println(cap(slice3)) // 10copy 方法
1
2
3
4
5slice3 := make([]int, 2, 5)
slice4 := make([]int, len(slice3), cap(slice3))
copy(slice4, slice3) // 可以认为是for循环两个slice中len最小的值进行赋值
slice4[0] = 1233
fmt.Println(slice3, slice4)// [0,0] [1233,0]copy方法使用两者长度较小的作为复制目标的长度- 方法返回复制目标长度,即二者长度最小值
Range
返回一个有序对
key, value用于遍历数组/切片/通道/集合,和python的enumerate方法类似例子
1
2
3
4
5
6
7
8
9
10nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
Map
- 无序键值对的集合,使用哈希表来实现
- 声明方式
var map1 map[string]sttring// 不赋初值声明map2 := make(map[string]string)// 不赋初值声明map3 := map[string]string{"a": "apple", "b": "banana"}// 赋初值声明
- 赋值方式与
python相同 - 可以使用
delete()函数删除字典中的键值对
强制类型转换
- 使用类似
float32(sum)的方法对var sum int = 2进行强制类型转换
接口
通过定义接口,然后直接实现接口中声明的函数,进行多态实现
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38type phone interface {
getname() string
call()
}
type Iphone struct {
}
type Nokia struct {
}
func (iphone Iphone) getname() string {
return "I am iphone"
}
func (nokia Nokia) getname() string {
return "I am nokia"
}
func (iphone Iphone) call() {
fmt.Println("call with iphone")
}
func (nokia Nokia) call() {
fmt.Println("call with nokia")
}
/* ===============in main() ====================== */
phone := new(Iphone)
fmt.Println(phone.getname()) //I am iphone
phone.call()// call with iphone
anotherPhone := new(Nokia)
fmt.Println(anotherPhone.getname()) // I am nokia
anotherPhone.call() // call with nokia
异常捕获
通过实现
error.Error()函数来实现异常捕获示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43type DivideError struct {
dividee int
divider int
}
func (divideError *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, divideError.dividee)
}
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
/* ===============in main() ====================== */
for divider := range []int{0, 1} {
if result, errorMsg := Divide(100, divider); errorMsg == "" {
fmt.Println(divider, result)
} else {
fmt.Println("errorMsg is: ", errorMsg)
}
}
/* output:
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
1 100
*/
Go 并发
goroutine
Go 使用 goroutine实现并发。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
开启goroutine的方法:
go fun(x, y, z)- Goroutine 会随着主线程的结束而自动销毁
channel
通道(channel)是用来传递数据的一个数据结构
使用方法
1
2
3
4ch := make(chan int) // 声明信道
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小
ch := make(chan int, 10)通道遍历可使用
range语法
实战
摘自公众号 Golang来啦
1. 下面代码能否正常结束
1 | func main() { |
对于range的循环次数,在最开始就已经确定,不会因为序列的变化而改变
2. 下面的代码输出是什么
1 | func main() { |
- 由于
range函数的返回会复用i和v而不是重新声明,因此在println函数输出前可能可以是循环过程中的任何值。为保证唯一,可以使用临时变量或者传参的形式保证传入参数的唯一性。
1 | func change(s ...int) { |
- 切片的底层是一个结构体,包含切片长度、容量和一个数组指针。对切片进行
拷贝或[i:j]截取操作时,底层的数组指针不会改变,仍指向同一数组。仅当在append操作使得len > cap时才会重新创建新的切片。因此对于副本的所有操作均会应用到原切片 - 注意区分切片的声明和数组的声明
3. defer 和 recover
defer
注册延迟调用的机制
把函数压入栈中,当defer的上层函数返回(包括正常返回和异常返回)后再将栈内函数弹出执行
由于是在真正返回之前进行弹栈,因此就可以通过诸如如下形式,对返回的命名参数在真正返回之前进行操作
1
2
3
4
5
6
7func fn()(r T) {
// init t
defer func() {
// do something change r to change the return value which is t before
}
return t
}拆解:将函数的返回命名为声明中的返回查看结果。对于匿名返回的参数,可将返回值赋给一个命名变量用来拆解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 第一个例子:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
// 拆解后:
func f() (r int) {
t := 5
// 1. 赋值指令
r = t
// 2. defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
func() {
t = t + 5
}()
// 3. 空的return指令
return
}综上所述,
- 对于命名返回参数的函数使用拆解的方法看
- 对于函数式的方法应该注意是传参调用还是闭包调用,传参使用的是复制的当时的值,闭包则使用的是变量的引用
defer延时调用的方法,可以在参数出现时定义其最终关闭的方法,防止后续赋值导致其失效的问题defer调用失败,会返回panic
recover
如果程序出现致命错误,触发
panic,此时当前正在执行程序会被直接停掉,而不只是协程。为了防止程序直接挂掉,需要在defer中使用recover对程序进行会恢复,防止程序完全挂掉recover只有在defer的上下文中才有效- 通过
recover捕获panic稳住主流程,以免影响其他协程的正常运行 recover函数和panic可以认为是弹栈和压栈操作,panic压栈interface,对于生效的recover再弹栈inteface
4. 闭包
- 闭包 = 函数 + 引用环境
- 匿名函数被称作闭包,只可直接调用或者赋值于某个变量
- 闭包捕获的变量和常量是引用传递
- 感性的理解上可以将闭包认为是声明了一个类,然后捕获的变量成为了类的成员变量
5. golang中... 的4处用法
- 函数中的最后一个参数,形如
func(n ...T)表示函数在此参数(可变参数)后可接受若干个本类型的参数 - 在向可变参数函数传递参数时使用
func(nums...)方式传入参数 - 在不定个数数组声明时,形如
[...]string{"Moe", "Larry", "Curly"}方式,使编译器自动判定数组长度 - 在go命令中使用
go test ./...的方式递归通配所有包文件
6. 下面代码输出什么
1 | func main() { |
- Map 的遍历是无序的,因此若A在第一次循环,则可循环3次,若A不在第一次循环,则循环两次
7. break 的高级用法
1 | func main() { |
8. 下面代码输出什么
1 | func main() { |
- 赋值顺序:先计算等号左边表达式取值,再考虑等号右边的赋值
9.下面代码输出什么
1 | ts := [2]X{} |
- 切片操作生成新的切片,使用
range返回参数循环变量均为变量副本,对其操作不会影响原切片 - 切片操作生成新的切片,但是共享底层数组,因此直接使用下标操作会同时生效
10. 下面代码有什么问题
1 | // 错误代码 |
- 在函数使用场景下,应注意先判断
error只有error != nil时,可认为返回值有效,然后进一步处理
11. 下面代码有什么问题
1 | func main() { |
- 调用
Done()后立刻调用Add(1)导致Wait()函数执行时,发现wg变量仍在被使用,误以为自己错误进入了Wait()函数而panic - 在使用
sync.WaitGroup时,Add(n)就要配对n个Done()调用,否则会出现死锁
12. 下面代码输出什么
1 | type Slice []int |
defer只会执行一级函数,对于循环调用,会提前计算到倒数第二级函数结果,仅在defer中执行最后一次调用
13. 下面代码输出什么
1 | func alwaysFalse() bool { |
首先注意到,第六行大括号独占一行。因此可以参考golang 代码断行规则
golang 在编译阶段,会在行尾是一下情况时,在其后插入分号
标识符
整数、浮点数、虚部、码点、字符串字面表示形式
字面表示形式可以理解为变量在代码中的表现,例如值为15但字面表示形式可以是15、0xF、0b1111等多种字面表示形式
break、continue、fallthrough、return
- ++、–
- )、}、]
- 为了允许复杂语句完全显示在一个代码行中,分号可能被插在一个右小括号
)或者右花括号}之前
对于上述情况外的其他情况,分号需要自行插入
对于可能隐式插入分号的断句可以使用合法的
,规避隐式分号的插入在上述代码中,
alwaysFalse函数只是被执行,但是没有被变量接收,等价于如下代码1
2
3
4
5
6
7
8
9
10
11
12
13func alwaysFalse() bool {
return false
}
func main() {}
switch _ = alwaysFalse();{
case false:
fmt.Println("false")
case true:
fmt.Println("true")
}
}上述代码直接进入
case true,若希望实现效果,应该改为如下形式1
2
3
4
5
6
7
8
9
10
11
12
13func alwaysFalse() bool {
return false
}
func main() {}
switch tmp := alwaysFalse()
tmp {
case false:
fmt.Println("false")
case true:
fmt.Println("true")
}
}仍然是个很奇怪的写法,使得代码可读性大大降低,非常不推荐
可以使用
go fmt和go vet命令进行代码格式化,增加代码规范性,发现可能的逻辑错误
14. 下面代码可以编译通过么
1 | func f(x int) { |
- 编译器会在7行和14行花括号前加入
;使得tag后生成一个由;单独构成的空语句,从而使得A和C两个tag合法 - 可在13行后加入
;使得语法编译通过
15. 下面代码输出什么
1 | func main() { |
- 输出空字符串,
TrimRight会将第二字符串中出现的全部字符从第一字符串右边开始匹配直到匹配不上为止 TrimSuffix可以完成去除后缀的效果
16. 下面代码输出什么
1 | var x int |
- 编译失败。
init函数不能被调用 main()函数不可带参数且不能有返回
17. 下面代码输出什么
1 | type User struct { |
- 编译失败,
SetName为User实现的函数,Employee是重新定义的类,需要重新实现SetName实例才可调用 - 修改方法
- 重写
func (e *Employee)SetName(name string)函数 - 将
User作为Employee的匿名变量,隐式调用User的SetName函数 - 同时使用1和2修改,导致两个
SetName的情况时,调用User的SetName需要显式调用
- 重写
18. 下面代码输出什么
1 | package main |
答案:[1,2,3][3,4,5]
解析:
- 知识点1:golang json库Unmarshal数组类型的逻辑。针对slice类型,在反序列化时,json库会首先将数组的len置0,然后将数据逐个append进数组。
- 知识点2:json库对未初始化slice的结构体字段,会初始化一个大小为4的slice,扩容逻辑为
newcap := v.Cap() + v.Cap()/2 - 题目解读:因此,当第一次unmarshal时,aa底层数组指向容量为4的数组。第二次unmarshal时,a结构体中的Child字段,发生一次扩容,放弃原有底层数组,复制数据后,重新指向一个容量为6的底层数组。因此,aa底层数组会保存有第二次反序列化时的前四个元素,同时a的Child字段slice地址也和aa不同。(此处的扩容逻辑是json库的扩容逻辑,需要注意与slice本身的appen过程<翻倍>扩容逻辑区分)
拓展:json库反序列化过程(翻译自Unmarsha源码注释)
接口:
json.Unmarshal([]byte(jsonStr1), &a),必须传入一个指针,否则抛出InvalidUnmarshalError异常Unmarshal与Marshal是互逆的操作,在有必要时,会申请map、slice和指针,并且还有如下附加规则:
对于所有反序列化为指针的json字段,如果json中为null字符则设置为nil指针;否则将结构体中指针指向对应值,如果结构体中对应字段为nil,则重新分配一个新的指针来指向对应值
对于实现了Unmarshaler接口的字段,Unmarshal方法会先调用该字段的UnmarshalJSON方法,即便json字符串中对应的值是null;否则,如果该字段实现了encoding.TextUnmarshaler接口并且输入也是一个带引号的字符串,Unmarshal方法会调用该字段的UnmarshalText方法,并将该值的去引号部分作为参数传入
如果要反序列化为一个结构体,Unmarshal会通过用于marshal的key来匹配(字段名称或字段的tag)优先使用精确匹配,同时也可以接受大小写不敏感的匹配情况。默认情况下,没有对应结构体字段的json key会被忽略,可通过 Decoder.DisallowUnknownFields更改配置
如果json字段对应的结构体字段是个interface类型,那么只会将对应字段值根据如下对应关系进行反序列化:
| go类型 | json类型 |
| ———————- | ——– |
| bool | boolean |
| float64 | number |
| string | string |
| []interface{} | array |
| map[string]interface{} | object |
| nil | null |当反序列化一个数组为slice时,Unmarshal方法会先设置其长度为0,然后再逐个元素append到slice中;有一种特殊情况,即当json字符串为一个空数组时,Unmarshal方法会将原有slice替换为一个空slice
当反序列化json数组到go数组时,如果go数组长度和json数组长度相同,直接赋值为对应go数组;若json数组长度较短,剩余go数组位使用0补齐;若json数组长度较长,则忽视多余部分
反序列化map时,会先建立一个map:如果go结构体的原map是nil,会分配一个新的map;不然会重用已有的map,写入新的k-v,同时保留已有的map键。map的键必须是一下类型之一:字符串、数字、实现了json.Unmarshaler或者实现了encoding.TextUnmashaler接口的结构体
如果json值和其类型不匹配,或者数值溢出,Unmarshal会跳过这个字段,并且会尽可能多的将合法的字段反序列化。如果没有更严重的错误发生的话,Unmarshal会返回UnmarshalTypeError说明此次错误。在任何情况下,在错误发生时,都不能确保其余字段能够正常的反序列化。
json的null或未设置的字段会反序列化为nil,并且不会抛出任何异常
如果反序列化带引号的字符串,无效的UTF-8或UTF-16字符不会被视为错误,而是会被替换为Unicode字符U+FFFD