浙江大学期末考试——Go语言
期末复习对学习知识的帮助是巨大的,相比于看视频,本篇点到为止的知识点形的期末复习笔记,我相信更能让人学会。我相信,只要把本笔记全部看完了必然就会Go了。如果你做到了但还没有学会Go,那你大可来找我/doge。
概论
- 指令:计算机的一个最基本的功能,如实现一次加法运算或实现一次大小的判别
- 计算机的指令系统:计算机所能实现的指令的集合
- 程序:一系列计算机指令的有序组合
算法:
求解特定问题的一组有限的操作序列
- 目的性:算法有运算结果,程序强调过程性
- 抽象性:算法独立于编程语言和指令系统
- 研究性:算法是理论研究,载体可以是伪码,文字,图片等,载体为某一编程语言时就是程序
基本特征:
- 有限性:一个算法在执行有限步之后必须会终止。
- 确定性:一个算法的每个步骤都必须精确地定义,可以严格地、无歧义地执行。
- 输入:一个算法在运行之前赋给它的量,或在运行过程中动态地赋给它的量。
- 输出:一个算法运行结束时的结果。
- 有效性:一个算法在运行过程中,所有运算必须是充分基本的,是可行的,原则上人们可以用笔和纸在有限的时间内精确地完成这些运算。
结构化程序设计
- 程序=算法+数据结构 ----获得图灵奖的Pascal之父Nicklaus Wirth
- 将复杂程序划分为若干个相互独立的模块
- 模块:一条语句(Statement)、一段程序或一个函数(子程序)等
结构化程序设计特点
- 自顶向下
- 模块化设计
- 结构化编码
OOP
- 封装和数据隐藏
- 继承和重用
- 多态性
GO 特性
静态编译型
语法强调少即是多
强调组合,更简洁的OOP
Duck模型的非侵入式接口
原生支持并发编程
支持多种操作系统和体系结构的交叉编译
大量使用内置函数和接口来提高代码复用度
支持和C 语言相互调用的机制(CGO)
语言环境变量
$GOROOT
GO 语言环境在计算机的安装位置
$GOPATH
GO 语言工作目录,可以有多个
Go语言的源码文件有三大类:
- 命令源码文件,可执行的程序的入口
- 库源码文件,集中放置各种待被使用的程序实体
- 测试源码文件,用于对前两种源码文件中的程序实体的功能和性能进行测试
Go Token(标记)
- 关键字:25个
- 标识符:40个
- 分隔符
- 字面量
变量
- 变量代表某块内存区域
- 变量的使用包括两个分开的步骤:变量声明、变量赋值
- 变量声明后会立即分配存储空间
- 变量声明后会初始化为该类型的零值
- 同一代码块内不能多次声明同名变量
- 子代码块可声明父代码块同名变量,子遮盖父
- 变量未声明直接使用会出现编译错误
- 变量在函数中声明了但未使用也会出现编译错误
- GO 是强类型语言,编译器会确认每个变量应有的类型,错误使用将引发错误
- GO 是静态语言,但支持编译时自动推断类型
- 变量声明后需按对应类型赋值
- 变量声明赋值可以同时进行
- 变量声明赋值同时进行可以忽略类型,由编译器推断
- 可在函数内部使用 := 进行短类型声明赋值
注:
- 多变量同时赋值,只能在函数体内。
a, b = 3, 4
- 短类型声明赋值,只能在函数体内。
c := true
- 全局变量可以不被使用
1 | package main |
iota枚举
Go里面的关键字iota
,可以在声明enum
时采用,它默认开始值是0,每调用一次加1:
1 | const( |
除非被显式设置为其它值或
iota
,每个const
分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota
,则它也被设置为iota
。
语法
条件语句
if
- GO不支持 ?: 三元运算符
- if 后面的条件不需要( )
- if 可带一个初始化子语句用;跟条件分开
switch
case 按照从上到下的顺序进行求值,直到找到匹配的项后执行并退出switch(除非使用fallthrough)。如果 switch 没有表达式,则对 true 进行匹配,因此,可以将 if else-if else 改写成一个 switch。
- switch 一样可以带初始化子语句
- switch 条件表达式不要求必须为整型,类型本身也可作条件判断
- case 后的break 可以省略
- 多个case连在一起是为了满足连续的条件范围
- 也可以直接把多个case条件连在一起,在最后一个case写执行语句
1 | var a = "hello" |
注:新编写的代码,不建议使用 fallthrough。
循环语句
for
- GO 的循环语句只有for ,没有while/do while
- ★:for 语句后面不能加( )
- for语句的三个部分,省略任何一个时,分号不能省略
- 只留条件判断时,可以不用分号 (相当于while语句)
- 全部省略,变为无限循环
转移语句
-
break:语句用于跳出代码块或循环, 除了用在switch之外,还用于结束整个循环,不再进行循环条件判断
-
continue:语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环
-
标签
如果存在多重循环,默认情况下break语句和continue语句都只针对最内层循环。
所以Go提供了标签,标签允许指定跳出的循环
1
2
3
4
5
6
7
8
9
10
11
12
13// 使用标签
flag := false
end:
for i := 1; i < 100; i++ {
for j := 1; j < 100; j++ {
if i*j == 651 {
flag = true
//一次跳出
break end
}
}
}
fmt.Println(flag)
数据类型
分类
- 命名类型:
- 基础数据类型(整型、浮点型等)
- 用户自定义类型(type关键字定义的结构、接口等)
- 未命名类型
- 集合类型(数组、切片、映射等)
- 函数等
Bool类型
- 布尔值包括true、false,类型长度为1字节
- 布尔类型无法被其他类型赋值,也不支持类型转换
- 布尔类型不支持用0和1表示真假
注:由于 Go语言是强类型的语言,如果不满足自动转换的条件,则必须进行强制类型转换。(C/C++等语言有隐式类型转换,golang中没有,即无法自动强转)
字符类型
- byte ,对应整型里的uint8 ,代表 ASCII 码一个字符
- rune,对应整型里的int32 ,代表Unicode码一个字符
格式化输出时,可用%c 输出对应值,如
1 | //浙 |
字符串类型
- 字符串类型string,采用UTF-8编码格式的不可改变的字符序列
- 字符串单行用双引号 ,多行可用反引号`,空格和缩进都会被保留
字符串标准库:
- strings包提供了很多操作字符串的简单函数
- strconv包提供了基本数据类型和字符串之间的转换
- regexp包提供了正则表达式功能
- unicode包及其子包 utf8、utf16 中,提供了对 Unicode 相关编码、解码的支持
strings包
1 | // 子串 substr 在 s 中,返回 true |
unicode 包主要包含3个部分:
- unicode :基本的字符判断函数
- utf8 :负责 rune 和 byte 之间的转换
- utf16 :负责 rune 和 uint16 数组之间的转换
1 | func IsControl(r rune) bool // 是否控制字符 |
运算符
- 算术运算符
- 自增、自减,直支持a++,不支持++a ==> 设计思想是保证只有一种写法
- 关系运算符
- 因为bool类型不能转为整型,所以不支持不等式连写:比如 x<y<z 这种语法是错误的
- 逻辑运算符
- 注意短路
- 赋值运算符
- 位运算符
注:Go中的运算优先级跟C不太一样,比如Go中位移运算符的优先级是高于算数运算符的,而C中相反
1 | // c++ --> 2 |
见:
集合数据类型
数组(Array)
- 数组是同类元素的集合,它的元素排列在连续的空间中,按下标来标记和访问
- 数组类型定义包括元素类型,数组长度(元素个数)
- 元素类型相同的两个数组,数组长度不同则类型不同,相互不能复制
- 数组变量声明后,其元素类型、数组长度均不可变 (定长性)
注:数组传参问题——实参拷贝一份给形参,二者相互独立,传递大数组时效率较低,尽量用指向数组的指针来传参
数组声明
1 | //只声明未赋值 |
切片( Slice)
数组的定长性和值拷贝限制其使用
- 切片封装底层的数组,提供长度可变的数组引用
- 切片是引用类型,不支持==运算(除了nil)
- 切片包括三个变量
- 底层数组指针
- 切片当前长度
- 切片容量(小于等于底层数组长度,超过时要变更底层数组)
切片使用
1 | // 创建指定的底层数组,然后创建切片 |
语法糖...
1 |
|
映射( Map)
映射用于存储一系列无序的键值对
- 映射的键(key) 只支持值类型(可以使用==运算符作比较)
- 映射的值(value)不限制,但所有元素的值类型一致
- GO 映射的底层实现是哈希数组链表,不是 C++的红黑树
注:
-
只声明不初始化的映射为nil值,未分配底层存储空间,不能添加元素
-
用字面量或make函数进行初始化后可以添加元素
即:
1
2
3
4
5
6
7
8
9
10
11
12
13
14var m1 map[string]int
fmt.Println(m1 == nil)
//true
//m1["a"] = 1 //error
m2 := map[string]int{}
fmt.Println(m2 == nil)
//false
m2["a"] = 1 //ok
m3 := make(map[string]int)
fmt.Println(m3 == nil)
//false
m3["a"] = 1 //ok
Map映射元素查找
-
映射元素通过下标直接查找访问
- 存在对应key的,返回对应value
- 不存在对应key的,返回value 类型的零值
==> Q: 如果返回零值,那如何判断是否存在呢?
A:映射元素通过下标访问其实可以返回两个值
- 对应的value
- 对应的key是否存在的布尔值
映射元素删除:使用内置函数delete()删除映射元素,如delete(personSalary,"steve")
二维映射map创建:
1 | comEmp := map[string] map[string]int { |
若顶级类型只是一个类型名,你可以在文法的元素中省略它。
1 | // demo1 |
函数
概念:
函数是程序执行的基本语法结构:
- 函数或方法编译成程序体代码区的一段指令序列
- 进程执行模型大多基于“栈堆”
- 函数抽象逻辑模块
- 通过函数调用函数,层层叠叠的函数构成树结构组织代码
- 函数效率高则程序效率高,建议多用标准库函数
函数为第一等公民(与其他数据类型地位平等)
- 赋值给变量
- 作为参数传递给其他函数
- 作为函数的结果被返回
- 支持闭包
语法格式:
1 | func add(a, b int) int { |
- 函数可以有多个返回值
- 返回值可以有变量名
- 不支持函数重载
函数参数:
- 参数传递方式都是值拷贝
- 形参与实参是值传递时,形参的变化不影响实参
- 形参与实参传递指针时,形参的变化有可能会影响实参
- ===> 注意:引用类型其实传的就是指针
- 不支持默认值参数
不定参数
- 不定参数,形参数目可变、不确定
- 不定参数声明语法格式:
param … type
- 不定参数类型必须相同
- 不定参数必须是函数的最后一个参数
- 不定参数的形参在函数内是切片
- 切片传递给不定参数时,要用…运算符取出各元素 ==>多于函数定参数的参数将会被放到不定参数中
1 |
|
函数类型
- 函数类型又叫函数签名,显示函数类型fmt.Printf("%T\n", funcname)
- 函数类型包括形参列表和返回值列表
- 形参列表:形参的次序、个数和类型(形参名无关)
- 返回值列表:返回值的次序、个数和类型(返回值名无关)
- 可以使用type定义函数类型
- 函数类型是引用类型,未初始化的零值为nil
- 标准定义的函数名为常量,不可修改指向
- 函数是第一公民,函数变量可赋值、传参等
匿名函数
匿名函数相当于函数字面量,可以使用函数变量的地方就可以使用匿名函数
1 | //匿名函数直接调用 |
闭包
闭包=函数+引用环境,常见于匿名函数引用了函数定义环境的变量
- 如果函数返回的闭包引用了该函数的局部变量
- 每次调用函数都会为局部变量分配内存
- 每次使用闭包都会影响局部变量
返回值
允许返回指定变量名
1 | func addT1(a, b int) (int, bool) { //多值返回,返回值不命名 |
函数作为类型,以及函数赋值给变量
1 | func add(a, b int )int{ |
defer
Go 函数支持defer进行延迟调用
defer 类似OO语言异常处理中的finally子句,常用来保证系统资源的回收和释放
- 在注册defer函数时,会把当时的实参值传递给形参,后续实参的变化不影响函数结果,如
1 | a:=5 |
- 使用多个defer时,这些defer 调用 以**先进后出(FILO)**顺序在函数返回前被执行
结构体
- 结构把有内在联系的不同类型的数据统一成一个整体,使它们相互关联
- 结构是变量的集合,从外部看是一个实体
- 结构支持嵌套
- 结构的字段类型不限
- 结构的存储空间连续,按声明时的顺序存放
使用:
命名类型结构
1 | type Employee struct { |
匿名类型结构(直接创建结构变量)
1 | var myemployee struct { |
带标签的结构体
- 标签是一个附属于字段的字符串,用于描述字段信息
- 标签还可以按
key1:“value1” key2:“value2”
键值对进行修饰,来提供编码、解码、ORM等转化辅助,比如JSON、BSON 等格式序列化
结构变量初始化
-
用命名类型结构或匿名类型结构声明的结构变量,各字段初始化为相关类型的零值
-
按字段名初始化,不用按顺序,未指定的字段为零值
-
1
2
3
4
5
6emp1 := Employee{
firstName: "Sam",
age: 25,
salary: 500,
lastName: "Anderson", //逗号不能忽略
}
-
-
用字面量初始化,按字段类型声明顺序并全部设置,顺序不对或遗漏字段报错
emp2 := Employee{"Thomas", "Paul", 29, 800}
访问结构体数据
-
采用 结构变量.字段
-
1
2emp := Employee{"Thomas", "Paul", 29, 800}
fmt.Println(emp.age)
-
-
采用(*结构变量指针).字段 *为间访符
-
1
2emp := &Employee{"Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", (*emp).firstName)
-
-
采用 结构变量指针.字段,不支持->
-
1
2emp := &Employee{"Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", emp.firstName)
-
匿名字段:结构体字段也可以省略字段名,字段名默认为对应数据类型名称(数据类型不能重复)
1 | type Person struct { |
嵌套结构
1 | type Address struct { |
子结构字段提升
匿名子结构的字段可以像父结构的字段一样被父访问(没有同名父结构字段)
1 | type Address struct { |
方法
方法是对具体类型行为的封装,本质上是绑定到该类型的函数
-
非命名类型不能定义方法
-
OO语言的方法通常有个隐藏的this或self指针来指向对象
-
GO 把这个隐藏指针暴露出来,称为接收者(receiver)
-
接收者可自定义名称,类型有值类型和指针类型两种,语法格式:
- 值类型:
func (t Type) funcName(paramList)(resultList)
- 指针类型 :
func (t *Type) funcName(paramList)(resultList)
方法接收者
-
方法接收者的本质是形参
- 方法接收者为值时,方法修改对象属性将不能成功
- 方法接收者为值时,需要在内存复制一份对象,效率低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//方法接收者是值
func (e Employee) changeName(newName string) {
e.name = newName
fmt.Printf("\nEmployee name in func is: %s", e.name)
}
//方法接收者是指针
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
// 实际上是拷贝了一个e对象,然后吧这个对象的name改掉了
// Print(e.Name)实际上还是没有修改
e.changeName("Michael")
// 而接收者为指针的函数就能修改
e.changeAge(18)
// 除了上述调用以外,还可以通过类型调用的方式
Employee.changeName(e, 52) //类型调用
(*Employee).changeAge(&e, 52) //类型调用
- 值类型:
总结:Go中的方法实现跟C比较类似,都是在体外完成对方法的具体实现。C是在类中声明函数原型,在类外以Class::FuncMethod(){ xxx }
中具体实现函数。
1 | type Employee struct { |
Q:方法可用等价函数实现,为什么还要方法?
- GO 的函数不能重载(即不准存在只是参数不同的同名函数),导致不同类型不能用同名函数,而不同类型的方法可以同名
- GO 不支持class ,使用结构代替类,结构字段用来封装对象属性,方法用来封装对象的行为
方法提升
匿名子结构的方法可以像父结构的方法一样被父使用(没有同名父结构方法)
1 | type Address struct { |
自定义类型扩展方法
方法并非结构体专有,所有自定义类型都可以定义方法
1 | type myInt int // 命名类型 |
方法值
- 方法本质上还是函数,所以方法可以赋值给函数变量
- 方法值是对象变量初始化后的方法
- 方法值其实是带有闭包的函数变量,接收者被隐式地保存在闭包里
- 方法值赋给函数变量后,函数变量可直接调用
- 方法也可以直接通过类型来调用,把接收者作为第一个参数:
e.play() <==>Employee.play(e)
,(*Employee).changeAge(&e, 52) //类型调用
接口
- 接口是编程规约,一组方法签名的集合
- 方法声明的两个组件构成了方法签名 - 方法的名称和参数类型。
- 传统OO里,接口通常用来抽象定义对象的行为,具体过程在类里实现 ,类在定义时要声明实现了哪些接口
- GO 接口采用非侵入式,即具体类型实现接口不用显式声明,只要其方法集是接口的超集,编译时会进行对应校验
- GO 接口只有方法签名,没有数据字段,没有函数体代码
- 类型的方法集是多个接口的超集,则实现多个接口
类型定义:
-
命名接口类型
-
1
2
3
4type interfaceName interface{//接口类型命名通常以er为后缀
methodName(paramList)(resultList)
otherInterfaceName
}
-
-
匿名接口类型
-
1
2
3
4interface{
methodName(paramList)(resultList)
interfaceName
}
-
空接口
- 空接口 interface{} 是一种匿名接口类型
- 空接口的方法集为空,所有类型都实现了空接口,都可以赋值或传递给空接口
- 非命名类型不能定义自己的方法,其方法集为空,只能传递给空接口==>map,slice
- 方法需要接收类型参数,所以非命名类型不能定义自己的方法
接口初始化
- 只声明未赋值的接口变量为nil
- 接口变量初始化需要把接口绑定到具体类型实例
- 未初始化的接口变量不能调用其方法
- 方法的接收者才能给接口变量赋值
- 接口变量的值包括底层类型的值和具体类型
1 | type Printer interface { |
在内部,接口值可以看做包含值和具体类型的元组:(value, type)
接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。
1 | type I interface { |
GO 的面向对象
Go 没有类,而是松耦合的类型、方法对接口的实现
封装
- 用结构代替类
- 用New函数代替构造函数
继承
- 用类型组合来实现继承
- 多重继承通过内嵌多个类型实现
多态
类型断言
Go语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T)
,这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
下面有个例子。只有当某个interface{}的类型 存储的是int时才打印出来。
1 | package main |
一个比较典型的应用场景就是:errors.(*MyError)
反射
- 反射可以在运行时检查变量的类型和值,是元编程的一种形式,在没有源代码时帮助调试程序
- 反射包 ” reflect” 通过空接口获取变量的类型和值
- 用接口实现多态
- 实例可以赋给它所实现的任意接口类型的变量
- 反射包 ” reflect” 通过空接口获取变量的类型和值
- func TypeOf(i interface{}) Type
- func ValueOf(i interface{}) Value
- reflect.Type 和 reflect.Value 各有许多方法,比如kind方法用于返回底层类型名称的常量
1 | func add(a, b int) int { |
错误处理
- 传统OO里,异常是一种程序控制机制,依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈
- GO 语言里没有异常机制,只有错误处理,错误通过函数的多返回值来处理
- GO 语言的错误主要有:
- 编译错误
- 运行时错误
- 逻辑错误
- GO错误处理方式
- 错误可处理,通过函数返回错误进行处理
- 错误不可处理,通过panic抛出错误,退出程序
错误实现
通过error 接口 实现错误处理的标准模式,打印错误时自动调用Error()函数
1
2
3
4 > type error interface{
> Error() string
> }
>
实际使用
1 | type PathError struct { |
创建错误
-
根据errors 包对错误的基本定义
-
1
2
3
4
5
6
7
8
9
10package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
-
-
fmt包的Errorf 函数
1 | func circleArea1(radius float64) (float64, error) { |
创建自定义错误
1 | package main |
自定义错误的实现逻辑:
- 自定义的错误都会重写Error() string的方法,即实现了error 接口。
- 在抛出自定义错误的函数中错误的返回值类型都是error接口
- 因此,在外部使用的时候
data, err = Myfunc()
,这边得到的err是error接口,至于捕捉自定义的错误就是通过对接口的类型断言来判断的了,即自定义的错误都能看到下面有if errObj, ok := err.(*MyError); ok
的代码
使用 goto 集中处理错误——Go特性
1 | // 常规的写法 |
panic(恐慌)
- 通常情况下,向报告错误状态的方式是返回一个额外的error类型值。但是,当遇到不可恢复的错误状态,导致程序不能简单继续执行时引发panic
- 引发panic的两种情况
- 主动调用panic 函数,会产生一个运行时错误,该错误提供RuntimeError() 方法用于区别普通错误
- 程序运行时出现未处理错误自动触发,比如当发生像数组下标越界或类型断言失败等运行时错误时,Go 运行时会自动触发panic
- 不应通过调用panic()函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式
panicking终止过程
- panic 类似异常会逐级上传
- 在多层嵌套的函数调用中触发或调用panic,会马上中止当前函数的执行,逐级冒泡上传到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况
recover
- panic一旦被引发就会导致程序崩溃,但无法保证程序不会发生任何运行时错误。
- recover专用于“拦截”运行时panic,让进入恐慌的程序恢复过来并重新获得流程控制权。
- recover 可以阻止panic继续向上传递
- ▲.为确保捕获panic, recover 必须在延迟函数(defer)中执行
总结:
- 程序发生的错误导致程序不能容错继续执行,应主动调用panic或由运行时抛出panic
- 程序发生错误,但能容错继续执行的,正常情况用错误返回值,运行时错误非关键分支用recover 捕获panic
1 | package main |
包
- GO使用包来组织源代码和代码编译,实现代码复用
- 任何源代码必须属于某个包,同时源码文件的第一行有效代码必须是package pacakge packageName 语句,声明自己所在的包。
- ▲.包名为 main 为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件
- 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下
包的引用格式
-
标准引用格式
-
1
2import "fmt"
fmt.Printf("Hello world!")通过:
库/包.func
的方式调用
-
-
自定义别名引用格式
-
1
2import F "fmt"
F.Printf("Hello world!")通过:
定义的名称,如F.func
的方式调用
-
-
省略引用格式
-
1
2
3import . "fmt"
//不需要加前缀 fmt.
Printf("Hello world!")
-
-
匿名引用格式
- 引用包,但是代码中却没有使用包,编译器会报错
- 在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式(
_
)
-
1
import _ "fmt"
init( )
- init( )是特殊的函数,不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高
- init( )常用于在开始执行程序之前对数据进行检验或修复,或者在程序开始之前调用后台执行的 goroutine
- 每个源码可以使用 1 个 init() 函数,一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序
包加载顺序
- 程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图
- 每个包会先①初始化常量,然后②是全局变量,③最后执行包的 init 函数
包内标识符导出——向外暴露
-
一个包里的标识符(如类型、变量、常量等)要被外部访问,需将要导出的标识符的首字母大写
1
2
3
4var myVar = 100 //内部引用
const MyConst = "hello" //导出
type MyStruct struct { //导出
} -
在被导出的结构体或接口中,如果它们的字段或方法首字母是大写,外部可以访问这些字段和方法
1
2
3
4
5
6
7
8type MyStruct struct { //结构体要被导出
ExportedField int // 包外可以访问的字段
privateField int // 仅限包内访问的字段
}
type MyInterface interface { //接口要被导出
ExportedMethod() // 包外可以访问的方法
privateMethod() // 仅限包内访问的方法
}总结:Go没有提供权限控制符,而是通过了首字母是否大小写的统一规定来加上权限控制,这个跟Python中私有权限加
__
,保护权限加_
颇为类似
go.mod
-
使用GOPATH 不太方便
-
go.mod是Go1.11版本新引入的官方包管理工具
-
在项目目录下用go.mod 文件来记录依赖包具体版本,方便依赖包、源代码和版本控制的管理、
-
go.mod 文件内容
- module:指定包的名字
- go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本
- require:指定的依赖项模块
- replace:可以替换依赖项模块
- exclude:可以忽略依赖项模块
-
go mod命令
go.sum 文件
- go.sum 文件 在执行 go get xxxx之后或直接编译使用第三方包的源代码时自动生成
- 详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改
go.mod 使用基本过程
- 创建项目目录Dir
- 在项目目录运行
go mod init projectName
,生成go.mod 文件 - 在项目目录执行 go get xxxx 下载第三方包, 会生成go.sum文件 (可选)
- 在项目目录下新建子目录,创建项目子包(可选,无需再生成go.mod)
并发
- 并行是在任一粒度的时间瞬间都同时执行,比如多机并行
- 并发是在规定的时间期限内多个任务都在执行,实际底层是分时操作
- 并行强调瞬时性、并发强调过程性
- 并行关键在于执行、并发关键在于结构
- 单机运行时,并行通过使用多处理器以提高速度,并发程序可以是并行的,也可以不是
- 应用程序具备好的并发结构,操作系统才能更好地利用硬件并行执行
进程( process )、线程( thread )和协程( coroutine )
- 进程是程序在内存中运行时,操作系统对其进行资源分配和调度的独立单位
- 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
- 每个进程至少包括一个线程
- 每个进程的初始线程被称为主线程,主线程终止,进程终止
- 协程是轻量级的线程,一个线程可以拥有多个协程
- 进程和线程是操作系统级的,协程是编译器级的。协程不被操作系统内核管理,而完全由程序控制,因此没有线程切换的开销。
- 和多线程比,数量越多,协程的性能优势就越明显。协程的最大优势在于其轻量级,可以轻松创建上万个而不会导致系统资源衰竭
go routine 特性
- 多数语言在语法层不支持协程,而是通过库方式,效率不高,容易阻塞
- Go 在语言级别支持协程
- 命名为goroutine,关键字go
- 由Go语言运行时统一调度,合理分配给各个CPU
- 各goroutine非阻塞,不会等待
- goroutine 可以并行执行
- goroutine执行的函数返回值被忽略===>因此需要得到返回结果的话需要通过chan
- 运行时不保证各goroutine的执行顺序
- goroutine之间被平等地调度和执行
- main函数会单独创建和分配一个go routine
协程间的通信——通道
- 通道是一种特殊的类型,同时只能有一个 goroutine 访问通道进行发送和获取数据。
- 通道是一个队列,遵循先入先出(FIFO)的规则
- 通道默认是阻塞的,使goroutine有效通信,不需要使用其他语言的显式锁或条件变量
- 通道是引用类型,需要使用chan关键字和内置函数make 进行创建
- 通道写入和读取使用
<-
运算符- 写入 :通道<-变量
- 读取: 变量<-通道
- 通道包括无缓冲通道和有缓冲通道
- 无缓冲通道 make(chan datatype)
- 有缓冲通道 make(chan datatype,capacity)
- 无缓冲通道只能存储一条消息,有缓冲通道可以根据make函数的capacity参数存储n条消息,按FIFO读出
- 缓冲与阻塞
- 无缓冲通道,写入等待读取,读取等待写入,在双方准备好之前是阻塞的
- 有缓冲通道,通道已满时的写入会等待,通道已空的读取会等待
单向通道
- 通道默认为双向的,单向通道只能用于发送或接收数据
- 所谓单向通道只是对通道作为函数参数的一种使用限制,跟C语言使用const修饰函数参数为只读类似
- 通常先创建双向通道,在函数形参中利用<-运算符修饰通道,使之变为只读或只写通道
func pull(ch <-chan int) //只读 func pump(ch chan<- int) //只写
- 关闭通道:
- 关闭通道使用内置函数close(),实际上是关闭写入,即发送者告诉接收者不会再有数据发往通道
- 接收者能够在通道接收数据的同时,获取通道是否已关闭的参数 v, ok := <-ch
for range
语句能自动判断通道是否已关闭
channel底层实现
src/runtime/chan.go:hchan定义了channel的数据结构
缓冲区是一个环形队列
一个channel只能传递一种类型的值,类型信息存储在hchan数据结构
1 | type hchan struct { |
通道底层实现 向channel写数据
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒
通道底层实现 从channel读数据
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
定时器
Timer
协程间的通信需设置超时等辅助机制
-
一次性定时器:定时器只计时一次,结束便停止
-
主要方法
1
2
3
4
5func NewTimer(d Duration) *Timer // 指定一个时间创建一个Timer,Timer一经创建便开始计时,不需要额外的启动命令
func (t *Timer) Stop() bool // 停止计时器,返回值true:定时器超时前停止, false: 定时器超时后停止
func (t *Timer) Reset(d Duration) bool // 停掉定时器,再启动,返回值同上
func After(d Duration) <-chan Time // 创建匿名不需控制的计时器
func AfterFunc(d Duration, f func()) *Timer // 延迟方法调用注:Timer一经创建便开始计时,不需要额外的启动命令
Ticker
周期性定时器:定时器周期性进行计时,除非主动停止,否则将永久运行
1 | func NewTicker(d Duration) *Ticker // 指定一个时间创建一个Ticker , Ticker一经创建便开始计时,不需要额外的启动命令 |
并发函数
WaitGroup
WaitGroup提供多个协程同步(平级)的机制,用来等待多个协程完成
信号量, Unix中保护共享资源的机制,用于防止多个线程同时访问某个资源
- 信号量>0,表示资源可用,获取信号量时系统自动将信号量减1
- 信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒
WaitGroup 的方法
- Add(delta int) 添加等待信号量
- Done() 释放等待信号,每次减少1
- Wait() 阻塞调用该方法的协程,直到等待信号量为0
1 | func process(i int, wg *sync.WaitGroup) { |
select
多路复用是在一个信道上传输多路信号或数据流,比如网线
- select 借用网络多路复用的概念,用于监听多个通道,同时响应多个通道
- 多个通道都没有可写或可读的状态,select 会阻塞
- 有一个通道是可写或可读的, select 会执行该通道语句
- 有多个通道是可写或可读的, select 会随机选择其中一个执行
1 |
|
context
-
WaitGroup用来控制多个平级goroutine同时完成
-
goroutine本身是平等的,但逻辑上可能有父子关系,context 表示程序上下文,是程序的运行状态,用来控制具有逻辑父子关系的多个goroutine
-
Context接口
1
2
3
4
5
6
7
8
9
10type Context interface {
//返回超时时间和是否已设置超时时间
Deadline() (deadline time.Time, ok bool)
//返回信道,当Context被撤销或过期时,该信道是关闭的
Done() <-chan struct{}
//Done信道关闭后,Err方法表明Context被撤销的原因
Err() error
//协程间的数据共享
Value(key interface{}) interface{}
}
context 使用
-
用context的树结构来给平等的goroutine 设置父子逻辑
-
context树的根节点通常是一个空的context ,由第一个goroutine用Background() 函数创建,该context不能被取消、没有值、也没有过期时间
-
创建子节点:
1
2
3
4
5
6
7
8//将父节点复制到子节点,返回一个可以主动撤销Context的函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
//过期时间由deadline和parent的过期时间共同决定,parent过期时间优先
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
//与WithDeadline类似,只不过传入的是从现在开始Context剩余的生命时长
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
//将父节点复制到子节点,子节点可扩展新的key存储的值
func WithValue(parent Context, key interface{}, val interface{}) Context
e.g.
1 | func HandelRequest(ctx context.Context) { |
注: 关于context.WithValue和context.Value的使用方式见:https://blog.csdn.net/u012190809/article/details/107700495
mutex
- 多个线程同时竞争使用某个变量可能会导致结果失控
- mutex,互斥锁,用来保证某个变量在任一时刻,只能有一个线程访问
- mutex 用Lock()和Unlock()来创建资源的临界区,这一区间内的代码是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码
- mutex 也可以用通道来代替,二者无优劣之分,通常不涉及线程交互数据的用mutex,其他用通道
1 |
|
RWMutex
-
RWMutex在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量
-
可以同时申请多个读锁
-
有读锁时申请写锁将阻塞
-
只要有写锁,后续申请读锁和写锁都将阻塞
-
主要方法:
1
2
3
4func (rw *RWMutex) Lock() //申请写锁
func (rw *RWMutex) Unlock() //释放写锁
func (rw *RWMutex) RLock() //申请读锁
func (rw *RWMutex) RUnlock()//释放读锁
e.g.
1 | func main() { |
Cond ( condition 条件变量)
-
互斥锁保证在同一时刻仅有一个线程访问某一个共享数据
-
条件变量在共享数据的状态发生变化时,通知其他因此而被阻塞的线程
-
条件变量需要用互斥锁来创建
-
主要方法:
1
2
3
4
5
6
7
8
9
10// 实例化一个带有Locker的Cond变量
func NewCond(l Locker) *Cond
//Unlock()->阻塞等待通知(等待Signal()或Broadcast()的通知)->收到通知->Lock()
func (c *Cond) Wait()
//激活执行Wait()的通知队列的第一个协程
func (c *Cond) Signal()
//激活执行Wait()的通知队列所有协程
func (c *Cond) Broadcast()
//使用内置的互斥锁
cond.L.Lock()和cond.L.Unlock()
e.g.
1 | var locker = new(sync.Mutex) |
附:
语言设计思想
-
保证只有一种写法
-
少即是多
-
GO 不支持class ,使用结构代替类,结构字段用来封装对象属性,方法用来封装对象的行为
-
用类型组合来实现继承
-
多态:Go中只要结构体实现了接口的部分方法,就可以通过接口指向该结构体,并且使用被实现的方法。因此,可以看到Go的代码中函数传参很多都是接口,但真正使用的时候传入的是实现了该接口的struct,这就是Go中多态提现的一个例子。
new 和 make区别
- new和make是内建的两个函数,主要用来在堆上创建分配类型的内存
- new用于普通类型的内存分配,内存清零,返回该类型指针
- make(专门)用于slice、map以及channel的初始化,返回引用
golang中分为值类型和引用类型
-
值类型分别有:int系列、float系列、bool、string、数组和结构体
-
引用类型有:指针、slice切片、管道channel、接口interface、map、函数等
-
值类型的特点是:变量直接存储值,内存通常在栈中分配
-
引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配
问题
Go中引用类型有哪些?
在 Go 语言中,引用类型有 切片 (slice) 、 字典 (map) 、 接口 (interface) 、 函数 (func) 以及 通道 (chan) 。
引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。
注:用户自定义类型不是引用类型===>所以函数传参的时候都传的是地址,形参都是指针类型。And: 引用类型不支持==
运算符,无法直接比较
总结:Go中规定的引用类型就5个:切片 (slice) 、 字典 (map) 、 接口 (interface) 、 函数 (func) 以及 通道 (chan),但传参为这几个类型的时候,不需要传指针,直接用相应的类型即可。
课上训练题
自定义排序
标准库的 sort 包采用的是快速排序
请模仿其实现,基于Sorter 接口实现冒泡排序
请按如下要求设计协程交互代码
- 协程一随机给出0~100的数
- 协程二猜协程一的数,猜中的话协程一返回信号0,结束程序;猜小了协程一返回信号-1,猜大了协程一返回信号1,继续猜
- 打印猜数过程
1 |
|
Go复习题目:
- Go 语言笔试面试题(基础语法)——基础知识题
- ★GO语言测试题
- 同学推荐的Go指南——可以根据概念实现做点题
考试中容易出的坑题:
- ▲与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换
- ▲与 C 不同,Go 没有指针运算。
容易踩的坑:
-
nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。
-
允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map添加元素则会造成运行时 panic
-
string 类型的变量值不能为 nil ==> string 的零值为""
-
访问 map 中不存在的 key,Go中返回的是零值,而不是报错
-
range后迭代对象:数组、通道、string、map
- 数组:for i, v := range arr,i为索引,v为arr[i]的值
- 通道:for data := ch, data为数据
- range 迭代 string 得到的值:
- range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。
- 注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。
- for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。
- range 迭代 map
- 如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。
- Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的
- 如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。
-
string 与 byte slice 之间的转换:Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:
- 在 map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
- 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {…}
-
在多行 array、slice、map 语句的末尾缺少 , 号
-
switch 中的 fallthrough 语句:switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。 <=等价于=>改写成 case 为多条件判断:
-
自增和自减运算: Go 特立独行,去掉了前置操作,同时 ++、-- 只作为运算符而非表达式。
-
不导出的 struct 字段无法被 encode。以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:
-
Go程序默认不等所有 goroutine 都执行完才退出
-
常用解决办法:使用 “WaitGroup” 变量,它会让主程序等待所有 goroutine 执行完毕再退出。
-
向已关闭的 channel 发送数据会造成 panic
-
在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:
-
▲若函数 receiver 传参是传值方式,则无法修改参数的原有值
-
从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:
-
跳出 for-switch 和 for-select 代码块:没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:
-
Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)
-
对Slice使用append,当长度超出时会返回新的Slice,因此无法在递归的时候不能直接传引用对象,而是需要传指针
*[]int
指针,并且在append的时候改成*ans = append(*ans, root.Val)
-
见Leetcode94树的中层遍历:
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
43
44
45func inorderTraversal(root *TreeNode) []int {
if root == nil {
return []int{}
}
ans := []int{}
OrderTravel(root, &ans)
return ans
}
/**
如果ans不传指针,而是直接ans []int传参,关注ans地址变化,输出地址得到:
append前:0xc000004078
append后:0xc000004078
append前:0xc0000040c0
append后:0xc0000040c0
append前:0xc0000040a8
append后:0xc0000040a8
*/
func OrderTravel(root *TreeNode, ans *[]int){
if root == nil {
return
}
OrderTravel(root.Left, ans)
*ans = append(*ans, root.Val)
OrderTravel(root.Right, ans)
}
/** 修改成传指针后ans的地址就始终是一致的了
append前: 0xc000004078
append后: 0xc000004078
append前: 0xc000004078
append后: 0xc000004078
append前: 0xc000004078
append后: 0xc000004078
*/
func main() {
root := &TreeNode{
Val: 1,
Left: nil,
Right: &TreeNode{
Val: 2,
Left: &TreeNode{Val: 3},
},
}
res := inorderTraversal(root)
fmt.Println(res)
}
-
from:Golang新手可能会踩的50个坑
Author: Mrli
Link: https://nymrli.top/2021/11/02/浙江大学期末考试——Go语言/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.