文章目录
学习总结
官方文档 —— 需翻墙
注意点
- 符号“{” 不能单独成行,必须跟在末尾
- 编译期会判断,自动在每一行语句后面加必要的分号“;”
- 需要多个语句写在同一行时,需要每个语句后加符号“;”
- 标识符严格区分大小写,首字符不能为数字,命名规则为“驼峰式”,不可与“关键字”或“预定义标识符”重复
- 变量、函数、常量名称的首字母也可以大写或小写
- 如果首字母大写,则表示它可以被其它的包访问(类似于 Java 中的 public);
- 如果首字母小写,则表示它只能在本包中使用 (类似于 Java 中 private)
操作符
普通操作符(不包括“运算符”,注意运算符的优先级)
操作符符号 | 作用 | 应用 |
---|---|---|
:= | 变量声明初始化,可以多个变量同时赋值 | a := 1; c,d := 1.0,“hello” |
= | 变量赋值(变量需要在此之前被声明),可以多个变量同时赋值 | a = 1; c,d = 1.0, “hello” |
‘’ | 单引号,定义字符,内容不会被转义(引号中的内容与实际的输出内容一致) | str := `he\nllo` |
`` | 反引号,定义字符串,内容不会被转义(引号中的内容与实际的输出内容一致) | str := `he\nllo` |
“” | 双引号,定义字符串,内容会被转义(引号中的特殊字符会被转义) | str := “he\nllo” |
T() | 类型强制转换,T 表示目标转换类型 | intVal := string(32) |
param.(T) | 类型断言与转换,判断变量param的数据类型是否实现了期望的接口或者具体的类型,T 表示期望的接口或具体类型。只有interface类型的变量才能使用断言。所以需要将先强转为interface类型,才能使用断言。可以使用“switch param.(type){}”确定变量类型。对值为nil的接口值进行断言会失败。 | valueOfInterface, isTypeMatched := interface{}(“interfaceOfStringValue”).(int) |
new(T) | 创建T类型的指针 | ptr := new(string) |
& | 取地址操作符,取出变量值的内存存储地址 | var big int16 = 1024; ptr := &big |
* | 1、放在变量前,为取值操作符,根据地址取出地址指向的值 | var big int8 = *ptr |
* | 2、放在类型前,用于声明变量的类型为指针 | var ptr *int8 = new(int8) |
_ | 空白标识符,任何赋给这个标识符的值都将被抛弃(这些值不能在后续的代码中使用) | var _ int16 = *ptr |
… | 1、数组的长度是根据初始化值的个数来计算,[…]指定的数组大小==初始化值的个数,只能使用简短格式声明,标准格式会报错(类型不匹配) | byteArr := […]byte{1, 2} |
… | 2、sliceParam…,对分片对象进行解包,得到切片中的所有元素 | sliceFive := append([]byte(“hellp”), []byte{1,2}…) |
… | 3、…T,声明变量类型为“可变参数类型”,指示函数传入的参数个数是可变的,参数本质上是一个数组切片。当 T 为 interface{},则可以传任意类型的数据。可根据“arg.(type)”断言,switch 出类型。 | func myfunc(args …int) {} |
运算操作符(优先级值越大,表示优先级越高)
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | && | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等(先比较类型,再比较值。都相同才称为相等。动态类型不可做等值比较,如map和切片) | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
关键字(保留字)
关键字 | 作用 |
---|---|
var | 声明变量(全局变量、局部变量) |
const | 声明常量(变量值需要在声明式指定) |
type | 声明类型(声明类型定义,或类型别名,注意类型别名不是类型,本质上类型仍然为原类型) |
struct | 声明自定义类型的结构体 |
range | 配合关键字 for 来迭代字符串\数组\切片\集合\通道里的每一个元素 |
defer | 延迟执行语句。代码在return之后执行(类似于finally), 存在多个延迟语句时,按照语句声明的先后顺序,从后往前执行。注意,只有defer后的第一个语句被延迟(多个执行语句时时可以写在函数中)。应用:关闭资源(文件流、连接) |
预定义标识符
预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用
测试一下好像有些可以重复,不深究。。。。
数据类型,builtin包中的部分内容
关键字 | 含义 |
---|---|
iota | 常量生成器 |
panic(interface{}) | 宕机,使程序终止运行 |
recover() | 恢复,防止程序崩溃,让进入宕机流程的goroutine恢复过来(仅在 defer延迟函数中有效),可以捕获异常的输入值。无异常时,recover()返回nil。注意异常后面的代码不会再执行 |
true | 无类型的布尔常量值 |
int | 不定长有符号整型 |
append | |
cap | |
close |
对象声明\初始化格式
零值(默认值)
声明时未指定初始值的变量,会自动根据变量类型,赋零值。
“零值”Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值
对象声明格式
1、标准格式
var 变量名 类型( = 表达式)
const 变量名 类型( = 表达式)
2、简短格式 —— 声明时必须指定初值,不能声明常量
变量名 := 表达式
- 同时满足以下限制:
- 定义变量,同时显式初始化。(该变量为初次声明)
- 不能提供数据类型。
- 只能用在函数内部。
3、批量格式
var(
变量名 类型 = 表达式
变量名 类型
变量名 = 表达式 // “无类型”变量
…
)
const(
变量名 类型 = 常量表达式
变量名 = 常量表达式 // “无类型”常量
…
)
初始化方式
make
- 只能对chan、map 以及 slice 的内存创建
- 返回的类型是引用类型,就是这三个类型本身
- 分配后会进行初始化(比如分配空间、预分配空间)
new(T)
- 可以分配任意类型的数据
- new 函数只接受一个参数,这个参数是一个类型
- 返回类型是指针,一个指向该类型内存区域起始位置的指针
- 会把分配的内存置为零,也就是类型的零值
var sum *int
sum = new(int) //分配空间
*sum = 98
fmt.Println(*sum)
常量与非常量
1、常量
const
- 需要通过关键字 const显示声明。
- 大多数为无类型常量: 没有声明类型的常量,声明形式: const 变量名称 = 变量值
- 常量间的运算(算术运算、逻辑运算和比较运算),运算结果也是常量
- 对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex 和 unsafe.Sizeof
- (iota应用) 如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的
- 批量声明常用来声明“枚举值”
2、变量(非常量)
var
- 可以使用 :=简短格式声明
数据类型
没有“枚举”类型,可使用常量+别名,来模拟模拟枚举
// 声明芯片类型
type ChipType int
const (
None ChipType = iota
CPU // 中央处理器
GPU // 图形处理器
)
func (c ChipType) String() string {// %s时调用
switch c {
case None:
return "None"
case CPU:
return "CPU"
case GPU:
return "GPU"
}
return "N/A"
}
func main() {
// 输出CPU的值并以整型格式显示
fmt.Printf("%s %d", CPU, CPU)
}
类型转换
不会隐式转换,只能强制转换
- byte <–> int
- byte <–> string
- bool 与 整型或字符或字符串 不可转换
- string 与 整型或布尔型 不可转换
- 整型 与 布尔型或字符串 不可转换
- 长度不同的数组之间 不可转换
参数传递类型
type Data struct {
first int
second int
arrow *byte
memberZ Member
memberY Member
}
type Member struct {
one string
}
值传递:param
- 新对象为原对象的深度拷贝值
- 新对象改动不会影响原对象
/* 所有值都是拷贝值,所以值的内存地址不同(再深层次的成员地址都是不同的) --> 所以无法修改 */
func valueCopy(obj Data) (d Data) {
ptrOneForMemberY := &obj.memberY.one
fmt.Printf("valueCopy: pointer=%p, value=%+v\n", ptrOneForMemberY, *ptrOneForMemberY)
cloneMemberY := "YYYY"
*ptrOneForMemberY = cloneMemberY
fmt.Printf("valueCopy: pointer=%p, value=%+v\n", ptrOneForMemberY, *ptrOneForMemberY)
return
}
func main() {
// 初始化对象
dataObj := Data{
first: 0,
second: 1,
arrow: nil,
memberZ: Member{one:"zh"},
memberY: Member{one:"yolanda"},
}
// 输出对象成员的内存地址和值
ptrOneForMemberY := &dataObj.memberY.one
fmt.Printf("initial: pointer=%p, value=%+v\n", ptrOneForMemberY, *ptrOneForMemberY)
valueCopy(dataObj)
fmt.Printf("initial: pointer=%p, value=%+v\n", ptrOneForMemberY, *ptrOneForMemberY)
}
引用传递(指针传递):¶m
- 新对象为原对象的指针,指向原对象的内存区域的起始地址
- 可以通过指针,对原对象的成员进行修改
/* 对象内存地址的拷贝值,所以由该地址找到的所有成员都是原对象的成员 --> 所以会修改原对象 */
func referenceCopy(obj *Data) (d Data) {
//obj = nil
fmt.Printf("referenceCopy: pointer=%p, value=%+v\n",obj, *obj)
obj.first++
fmt.Printf("referenceCopy: pointer=%p, value=%+v\n", obj, *obj)
return
}
func main() {
// 初始化对象
dataObj := Data{
first: 0,
second: 1,
arrow: nil,
memberZ: Member{one:"zh"},
memberY: Member{one:"yolanda"},
}
// 输出对象的内存地址和值
fmt.Printf("initial: pointer=%p, value=%+v\n",&dataObj, dataObj)
referenceCopy(&dataObj)
fmt.Printf("initial: pointer=%p, value=%+v\n",&dataObj, dataObj)
}
分类
1、数值类型
整数
- 定长有符号
int8, int16, int32, int64- 定长无符号
uint8, uint16, uint32, uint64- 不定长有符号
int- 不定长无符号
uint
浮点数(定长有符号)
float32
float64
复数(定长有符号)
complex64(32 位实数和虚数),
complex128(64 位实数和虚数)
2、布尔类型(定长)
bool
- true,false 为无类型常量
3、字符类型(定长)
byte(uint8, 代表一个 ASCII 字符),
rune(int32, 代表一个 UTF-8 字符)
- 利用“%T”打印数据类型时,为括号内的整型类型
4、字符串类型(不定长)
string
- 字符串中的每一个元素叫做“字符”
- 遍历方式
- ASCII 字符串遍历直接使用下标
for i := 0; i < len(theme); i++ {
fmt.Printf("ascii: %c %d\n", theme[i], theme[i])
}
- Unicode 字符串遍历用 for range
for index, s := range theme {
fmt.Printf("Unicode: %c %d\n", s, s)
}
5、指针类型(不定长)
*T,表示是T的指针类型
var big int16 = 1024
var small int8 = int8(big)
var ptr *int8 = &small// 指针类型为 *int8,用于接收变量的内存地址。 表示是int8的指针类型,*表示指针
6、数组类型(声明时指定数组长度)
- 多维数组:var array_name [size1][size2]…[sizen] array_type
- 长度相同、类型相同的数组对象,可以直接使用 ==!= 比较是否相等(所有值都相等时,值相等)
- 长度不同、类型相同的数组对象,不属于同一类型,不可进行强制转换
- 长度相同、类型相同的数组对象与切片对象,不可等值比较(数组类型与切片类型不可互相比较)
- 指定的数组长度表达式需要为省略号“…”(不能用标准声明格式)或常量表达式(在编译期就能确定值)
var byteOne [2]byte
byteTwo := [...]byte{0, 0}// ...
var byteThree [2]byte = [2]byte{0,0}
fmt.Println(byteOne==byteTwo, byteTwo == byteThree)// 输出:true true
// 多维数组
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var arrayOne [4][2]int
arrayOne[0][0] = 10
// 使用数组字面量来声明并初始化一个二维整型数组
arrayTwo = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
arrayThree = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 将 arrayThree 的索引为 1 的维度复制到一个同类型的新数组里
var arrayThree1 [2]int = arrayThree[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int = arrayThree[1][0]
7、切片(声明时不指定数组长度)
sliceVariable [开始位置 : 结束位置]
- 多维切片:var sliceName [size1][size2]…[sizen] sliceType
- 声明未初始化的切片对象,默认为 nil
- 区别:声明为初始化的数组对象(指定了数组长度),不为 nil,各索引位上的值为指定类型的默认值
- 初始化为空切片的对象,不为 nil
- 切片对象之间不可使用运算符“==” 进行等值比较
- 区别:数组对象之间可以使用运算符“==” 进行等值比较
- 从数组或切片生成新的切片 —— 通过给定开始与结束位置(包括切片复位)的切片,!不会!发生内存分配操作(只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置)
- 从指定范围中生成切片(不可越界):sliceVariable[1:2],sliceVariable[1:],sliceVariable[:2]
- 重置切片,得到空切片(注意切片内存地址的开始位置):sliceVariable[index:index]
- 克隆生成新切片(切片内元素相同,因为共享同一个底层数组): sliceVariable[:]
- 利用 make\append方法生成新的切片 —— make生成切片!一定发生了!内存分配操作
- 利用 copy方法复制切片(对象可以为 nil)
- 类型必须一致,长度可以不一致。
- 源对象长度a > 目标对象长度b,取源对象的前b个元素复制到目标对象中
- 源对象长度a < 目标对象长度b,取源对象的全部元素复制到目标对象的前b个元素中
- 可以对 nil切片进行 copy复制,无论作为源对象还是目标对象,都不会产生任何影响
- 类型必须一致,长度可以不一致。
- 切片的增删改通过上述修改切片的方法实现
// 声明初始化
var sliceOne []int// 声明未初始化的切片对象, sliceOne == nil 结果为true
var sliceTwo []int = []int{}// 初始化为空切片的对象,sliceTwo == nil 结果为false,但sliceTwo[0]时有数组越界异常
var sliceThree []int = []int{} // 切片对象之间不可使用运算符“==” 进行等值比较
// 从数组或切片生成新的切片
sliceFive := []byte("hello");
sliceFiveClone := sliceFive[:]
fmt.Println(sliceFiveClone[1], sliceFive[1])
sliceFiveClone[1] = 'z'
fmt.Println(sliceFiveClone[1], sliceFive[1])// 变量的内存地址一致
sliceFiveEmpty := sliceFive[0:0]
fmt.Println(sliceFiveEmpty == nil, sliceFive[1])// 得到空切片(非nil)
// 利用 make\append方法生成新的切片
var sliceFour = make([]int, 1, 10)
fmt.Println(sliceFour[0])// 越界:sliceFour[1]
sliceFive := append([]byte("hello "), []byte{1, 2}...)// 利用符号“...”对分片进行解包
// 多维切片
slice := [][]int{{10}, {100, 200}}// 声明一个二维整型切片并赋值
slice[0] = append(slice[0], 20)// 为第一个切片追加值为 20 的元素
// 声明处理函数链
chain := []func(string) string{// 成员可以是任意满足形参与返回值类型的函数的函数名
removePrefix,// 自定义函数
strings.TrimSpace,// 标准库函数
strings.ToUpper,
}
8、Map(又称关联数组、字典)
map
- 无并发控制,非线程安全。线程安全的Map为“10.应用.1.sync.Map”
- 声明(valuetype前可以有空格)
var mapname map[keytype]valuetype = map[keytype]valuetype{key1:value1, key2:value2}
var mapname map[keytype]valuetype = make(map[keytype]valuetype)
- 添加元素
mapname[key1]=value1
mapname[key2]=value2
- 遍历元组
var map1 map[int16]string = map[int16]string{1:"a",2:"b"}
var valueList []string
map1[3] = "c"
for k,v := range map1{
valueList = append(valueList, v)
fmt.Printf("key=%v, value=%v\n",k,v)
}
fmt.Println(valueList)
- 删除元组
delete(map1,1)// 删除map1中key=1的元组
- 清空所有元组:没有自带方法。推荐直接make新的对象,因为逐一删除效率远低于Go的并行垃圾回收操作的效率
9、通道channel
10、结构化类型(自定义类型)
声明类型别名
type TypeAlias = Type
- TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型(利用%T打印出的是Type)
type NewInteger = int32
声明类型定义
type TypeAlias Type// 不包含成员
type TypeAlias struct{// 包含成员,成员名称可省略
(member1) Type1
member2, member3,... Type2 // 相同类型变量可以声明在一起
...
(memberN) TypeN
}
struct{// 匿名结构体
(member1) Type1
...
(memberN) TypeN
}
type Brand int// 不包含成员
type BrandGroup struct {// 包含成员:struct
a int
string
}
匿名结构体
- 没有类型名的自定义结构
type BasicColor struct{
r int
g int
b int
}
func (b *BasicColor) paint(){
fmt.Println("print painting")
}
type Color struct {
// 至多只能包含一个属于同种类型的匿名变量。
// 多个匿名变量的内嵌成员(成员指:成员变量和成员方法)的名称重复时,无法通过编译
int
count int
// 外部结构的实例可以直接访问内部结构匿名变量的成员,但不可直接访问非匿名变量的成员(成员指:成员变量和成员方法)。
// 外部结构的实例也可以通过内嵌结构的类型名作为变量名,访问内部结构匿名变量的成员(成员指:成员变量和成员方法)。
one BasicColor
BasicColor
// 声明Location变量,类型为内嵌匿名结构体:该变量初始化时,需要重新声明结构体
Location struct{
x int
y int
}
}
func main(){
colorCollerction := new(Color)
colorCollerction.int=1
colorCollerction.BasicColor.r=2// 匿名变量的成员变量
colorCollerction.g=3// 匿名变量的成员变量
fmt.Printf("%+v\n",colorCollerction)
colorCollerction.paint()// 匿名变量的方法
colorCollerction.Location =struct {// 内嵌匿名结构体,初始化时需要再次声明结构
x int// 不能写逗号
y int
}{
x:1,// 要写逗号
y:2,
}
}
结构体的初始化
- 匿名结构体的初始化同理,变量声明时定义匿名结构体,变量的初始化可以在声明时,也可以在声明之后
- 利用“键值对”进行初始化
- 利用“多个值的列表”进行初始化,即忽略变量名(键)
- 利用“多个值的列表”进行初始化时需注意:
- 1、必须初始化结构体的所有字段。
- 2、每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 3、键值对与值列表的初始化形式不能混用
- 至多只能包含一个属于同种类型的匿名变量
- 多个匿名变量的内嵌成员(成员指:成员变量和成员方法)的名称重复时,无法通过编译
- 外部结构的实例可以直接访问内部结构匿名变量的成员,但不可直接访问非匿名变量的成员(成员指:成员变量和成员方法)
- 外部结构的实例也可以通过内嵌结构的类型名作为变量名,访问内部结构匿名变量的成员(成员指:成员变量和成员方法)
- 内嵌匿名结构体(定义在结构体内的匿名结构体)类型的变量,初始化时,需要重新声明结构体
type People struct {
name string
child *People
}
relation := &People{// 键值对形式
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{// 多个值的列表形式
"四川",
"成都",
610000,
"0",
}
- 以下属性的代码示例
- 非本地类型(?)不能自定义方法
- 不同成员含有相同的方法时,调用异常(无法确定是调用哪个成员的方法)
标签:Tag
`key1:“value1” key2:“value2”`
- 位于在结构体的字段后,最外层使用反引号 `
- 结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔(键与值之间只能用一个冒号间隔,冒号不允许出现空格)
type cat struct {
Name string
Type int `json:"type" id:"100"`
}
应用
1、sync.Map
- 线程安全,比map实现复杂、执行速率低,易引起运行时转换错误导致宕机
- 是sync包下的特殊结构,不是以语言原生形态提供,与map关键字生成的对象不同
- 只能使用 sync.Map 的方法实现键值对的新增、删除、修改、获取、遍历操作
- 只需声明变量,无需初始化
- 元素可以是任意类型:可以向一个实例中存储不同的任意类型的 key、value
var map2 sync.Map
map2.Store(1,"a")
map2.Store("1","a")
fmt.Printf("concurrent Map type is %T, value=%[1]#v\n",map2)
valueOfMap1, isExisted1 := map2.Load(1)
fmt.Printf("map[1]=%v, (key,value) exists: %v\n", valueOfMap1, isExisted1)
map2.Range(func(k,v interface{}) bool{
fmt.Printf("key=%v, value=%v\n",k,v)
if(k==1){
map2.Delete(k)
}
return true
})
fmt.Printf("concurrent Map type is %T, value=%[1]#v\n",map2)
valueOfMap2, isExisted2 := map2.LoadOrStore(1,1)
fmt.Printf("map[1]=%v, (key,value) exists: %v\n", valueOfMap2, isExisted2)
fmt.Printf("concurrent Map type is %T, value=%[1]#v\n",map2)
2、list(container\list)
- 易引起运行时转换错误导致宕机
- 是contailner包下的特殊结构,不是以语言原生形态提供
- 只能使用 container.list 的方法实现值的新增、删除、修改、获取、遍历操作
- 元素可以是任意类型
//var list1 list.List // 使用此声明形式时,无需初始化
list1 := list.New()
list1.PushBack(1)
ele := list1.PushBack("1")
list1.InsertBefore("2",ele)
list1.Remove(ele)
for element := list1.Front(); element != nil; element= element.Next() {// 遍历
fmt.Printf("%T value=%[1]v\n",element)
fmt.Printf("%T value=%[1]v\n",element.Value)
}
3、nil
- map、slice、pointer、channel、func、interface 的零值
- nil没有默认类型
- 不同类型的nil的指针是一样的:0x0
- 不同类型的nil的内存空间大小可能不同 —— 由类型决定,与同类型中的非 nil 类型的大小是一样的
- nil标识符不能比较(nil == nil),不同类型的nil变量不能进行等值比较,同类型也不一定可以(切片间不能进行等值比较)
- nil 不是关键字或保留字(我们可以定义一个名称为 nil 的变量,如 nil := “hah”,但并不提倡。声明nil变量之后,需要引用空值nil时,实际调用的是本地声明的nil变量)
var m map[int]string
var ptrInt *int
var ch chan int
var sl []int
var f func()
var i interface{}
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", m)
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", ptrInt)
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", ch)
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", sl)
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", f)
fmt.Printf("type=%T, pointer=%[1]p, value=%[1]#v\n", i)
11、函数类型:func
-
一组形参或返回值有相同的类型,不必为每个形参都写出参数类型
-
可以使用空白标识符“_” 声明参数(如形参、返回值),强调某个参数未被使用
-
每一次函数在调用时都必须按照声明顺序为所有参数提供实参
-
实参通过“值传递”的方式进行传递
-
因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。
-
支持多返回值
-
每个函数至多指定一个接收器
-
同一个包下,没有接收器的函数的函数名不可重复,但不同接收器间的函数名可以重复
参数声明的两种格式
- 适用于形参和返回值
- 两种形式不可混用,会报错:syntax error: mixed named and unnamed function parameters
func typedTwoValues() (z int, int) {// 异常
return 1, 2
}
1、同一种类型返回值
func typedTwoValues() (int, int) {
return 1, 2
}
func main() {
a, b := typedTwoValues()
fmt.Println(a, b)
}
2、带有变量名的返回值
返回值带有变量名时,return语句中可以忽略返回值(只需声明“return”即可)
func sub(x, y int, c, d string) (z int, v int) {
z = x - y // z 无需再声明
v = 100
return // 可以不指定返回值
//return -1,z// 指定返回值时,必须按顺序声明返回值
}
参数的值传递方式
见“数据类型.参数传递类型”
匿名函数
// 定义格式
func(参数列表)(返回参数列表){
函数体
}
声明调用方式
- 1、在定义时调用匿名函数
func(data int) {
fmt.Println("hello", data)
}(100)
- 2、将匿名函数赋值给变量
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
闭包(Closure)
函数 + 引用环境 = 闭包
- 引用了外部变量的匿名函数称为“闭包”
- 函数为“匿名函数”
- 引用环境为“被函数调用,且声明在函数外部的变量”
- 若匿名函数中,引用\调用了在匿名函数外部声明的变量,则该匿名函数称为“闭包”
- 变量不同,同一个匿名函数可以形成不同的“闭包”
变参函数
- 包含可变参数类型形参的函数
- 可变参数类型:…T,T表示数据类型,本质上是一个数组切片
- 当 T为 interface{}时,表示参数可以是任意类型的数据,可利用“switch param.(type)”获取param变量的实际类型
- “可变”指的是参数的个数可变
- 当且仅当数据类型为 interface{}时,可以使用断言param.(T)
- 在多个可变参数函数中传递参数,需要使用“…”进行解包
// 实际打印的函数
func rawPrint(rawList ...interface{}) {
// 遍历可变参数切片
for _, a := range rawList {
// 打印参数
switch a.(type) {
case int:
fmt.Println(a, "is an int value.")
case string:
fmt.Println(a, "is a string value.")
case int64:
fmt.Println(a, "is an int64 value.")
default:
fmt.Println(a, "is an unknown type.")
}
}
}
// 打印函数封装
func print(slist ...interface{}) {
// 将slist可变参数切片完整传递给下一个函数
rawPrint(slist...)// 利用 ... 对“切片”进行解包
}
func main() {
print(1, 2, 3)
}
模拟构造函数
- GO没有构造函数,可没有“重载”的概念
- 可以实现创建实例返回指针的方法,实现构造函数的功能
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color,
}
}
方法(结构体方法)
- GO方法是作用在接收器(receiver)上的一个函数
- 面向过程编程中没有方法的概念,只有“结构体”和“函数”的概念,可以通过“结构体+函数”,有使用者使用函数参数与调用关系来形成接近“方法”的概念 —— GO不是严格的面向过程或是面向对象
- 类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的
- 需要为基本类型添加方法时,可以为方法指定接收器类型为“基本类型的自定义类型别名”
方法的接收器
- 接收器类型
- 可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型
- 但不能是一个接口类型(因为接口是抽象的定义,方法是具体的实现)
- 也不能是一个指针类型(但可以是允许类型的指针,? 指针不就是指针类型吗。。。)
- 每个方法只能有一个接收器
- 接收器变量名,官方建议为接收器类型名的第一个小写字母,如Socket 类型的接收器变量应该命名为 s
- 接收器类型和参数类似,可以是指针类型和非指针类型
- 对于具体结构类型T,指针类型*T接收器的方法集与同一个类型的非指针类型T接收器的方法集不同,“非指针类型接收器的方法集”包含在“指针类型接收器的方法集”中
- 当接收器类型为指针类型时,利用接收器变量是结构体实例的引用,利用变量进行的修改会对该实例生成影响 —— 适用于大对象
- 当接收器类型为非指针类型时,接收器变量是结构体实例的值拷贝,利用变量进行的修改不会对该实例生成影响 —— 使用小对象
- 小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。
- 接收器强调方法的作用对象是接收器,也就是类实例(而函数没有作用对象)
- 类型 T(或 T)上的所有方法的集合叫做“类型 T(或 T)的方法集”,T 可以是具体类型或他的指针类型
// 面向过程实现方法 --> 缺少方法与类型的归属关系
type Bag struct {
items []int
}
// 将一个物品放入背包的过程
func Insert(b *Bag, itemid int) {// 将操作对象作为形参
b.items = append(b.items, itemid)
}
func main() {
bag := new(Bag)
Insert(bag, 1001)
}
// Go语言的结构体方法
type Bag struct {
items []int
}
func (b *Bag) Insert(itemid int) {// 指定接收器,表明方法的作用对象实例
b.items = append(b.items, itemid)
}
func main() {
b := new(Bag)
b.Insert(1001)
}
12、接口类型:interface
type InterfaceName interface {
methodNameOne(形参列表)(返回参数列表)// 参数可以不指定参数变量名称
methodNameTwo(形参列表)(返回参数列表)
...
}
- 接口不可声明变量(吗?),接口是抽象的定义
- 一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样
- 只要类型实现了所有内嵌接口的方法,则该类型实现了外层接口
空接口
interface{}
- 所有类型都可以实现了空接口类型,因此可以强转为空接口类型
- 应用:所有类型的变量需要断言时,都应该先强转为interface{}类型
- 任何类型的值都可以直接赋值给空类型变量,但空类型变量值需要类型强转后才能赋值给具体类型的变量
- 空类型变量可以被不同类型的值重复赋值
实现关系的检查者
- Go编译器将自动在需要的时候检查两个类型之间的实现关系
断言
value, ok := x.(T)
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T
- 对具体类型变量进行断言,返回对象的类型也是具体类型
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值
- 对接口类型变量进行断言,返回对象的类型也是接口类型
- 具体类型的指针变量,强转为接口类型后获取的指针变量,也是接口类型,断言返回接口类型变量
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败
- fmt.Println(interface{}(nil).(int)) // 异常: interface {} is nil, not int
// 类型定义见下文
// impAll := GameService{}// typeService为具体类型GameService
impAll := new(GameService)// typeService为接口类型Service
switch typeService:=interface{}(impAll).(type) {
case Service:
fmt.Println("Service")
case GameService:
fmt.Println("GameService")
case ImplementAllService:
fmt.Println("ImplementAllService")
default:
fmt.Printf("%T,value=%+[1]v\n",typeService)
}
类型实现接口的条件
结构实现接口的“类型转换”的问题讨论:https://stackoverflow.com/questions/40823315/x-does-not-implement-y-method-has-a-pointer-receiver
- 接口的方法与实现接口的类型方法签名一致
- 方法签名一致:方法名称、参数列表、返回参数列表
- 接口中所有方法均被实现:不一定需要由一个类型完全实现
- 接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的
// 一个服务需要满足能够开启和写日志的功能
type Service interface {
Start() // 开启服务
Log(string) // 日志输出
}
// 日志器
type Logger struct {
}
func (g *Logger) Log(l string) {// 实现Service的Log()方法
}
// 游戏服务:通过内嵌Logger类型的成员,实现了Service类型的所有方法 --> 类型GameService实现了接口Service
type GameService struct {
Logger // 嵌入日志器
}
func (g *GameService) Start() {// 实现Service的Start()方法
}
// 结构的方法集包含了接口的所有方法,但部分属于类型的方法集,部分属于指针类型的方法集
type ImplementAllService struct {
}
func (i ImplementAllService) Start(){// 属于类型的方法集
fmt.Println("start ImplementAllService")
}
func (i *ImplementAllService) Log(l string){// 属于指针类型的方法集
fmt.Println(l)
}
类型与接口的转换
- 接口类型interface{}的指针类型 *T,可以强转为类型实现的接口类型 InterfaceName
- 接口类型interface{}的类型实例 T,不一定可以强转为类型实现的接口类型 InterfaceName:只有当接口的所有实现方法都属于类型的方法集(区别与指针类型的方法集)时,才可以进行强转
- 类型实现的接口类型的指针 *InterfaceName,可以强转为指针类型 *T,必须是要转换的对应的指针类型(转换目标的类型不可以是其他实现了该接口类型的类型)
- 实现了接口的类型的实例变量,可以直接赋值给接口类型的变量
- 非侵入式设计:隐式实现接口,无须让实现接口的类型写出实现了哪些接口
- 接口中定义了多个方法时,类型需要实现接口中的所有方法, 才能说该类型实现了接口
- 只有类型的方法集包含接口的所有待实现方法时,该类型的实例变量值才可以直接赋给被实现接口的实例变量
- “类型的方法集”:注意类型可以是具体类型,也可以是他们的指针类型
- “具体类型的方法集”不同于“指针类型的方法集”
- “具体类型的方法集”包含在“指针类型的方法集”中,“指针类型的方法集”不包含“具体类型的方法集” 文档链接:搜索关键字“ promoted methods”
- 因此,最好使用“指针变量”进行“类型转换”
- 任意类型都可以强转为 interface{} 类型
- 但转为具体的结构类型或接口类型(断言)时,需要看是否满足一定的关系
- 任意类型都可以强转为 interface{} 类型
- 若接口的所有方法(多个)分别属于两个种接收器类型(指针类型和非指针类型),则
- 该具体类型的实例不可以被强制转换为接口类型(其实例的指针可以)
- interface{}(structInstance).(InterfaceName) // 异常
- iinterface{}(&structInstance).(InterfaceName) // 正常
- 该具体类型的实例不可以直接赋值给被实现接口的变量,但具体类型的指针可以直接赋值给被实现接口的变量,且接口变量可以调用该接口的所有方法,方法实现体为具体类型的方法
- interInstance := structInstance // 异常
- interInstance := &structInstance // 正常
- 该具体类型的实例不可以被强制转换为接口类型(其实例的指针可以)
- 因此,最好使用“指针变量”进行“类型转换”
type GameClone struct{// 继承了成员GameService的方法,因此也实现了接口Service
GameService
}
func main(){
//game := GameService{Logger{}}// 类型T不可转为Service
game := new(GameService)// 指针类型*T可以转为Service
insertionGame := interface{}(game).(Service)// 类型不匹配时 panic
//ptrOfGame := insertionGame.(*GameClone)// main.Service is *main.GameService, not *main.GameClone
fmt.Printf("存储地址为:%T,value=%+[1]v, 强转后存储地址为:%T,value=%+[1]v\n",game,insertionGame)
}
类型与接口的关系
- 一个类型可以实现多个接口
- 一个接口可以被多个类型实现
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}
func main() {
f := new(file)// 创建变量,变量类型实现了接口
var writer DataWriter// 声明一个接口的变量
writer = f// 将接口赋值f,也就是*file类型
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}
流程控制
结构 —— 都不需要括号
if…else if… else…
if 变量声明初始化语句;条件表达式 {// 可以加变量声明初始化语句,需要初始化多值时,需要使用多值同时赋值(a,b := 1, 2)
// TODO
}else if 条件表达式{
// TODO
}else {
// TODO
}
switch…case…default…
// case内为独立代码块,执行结束后,不会像C++\java一样,不会继续执行下一个代码块
// 但可以使用关键字“fallthrough”,强制本代码块执行结束后进入下一个case\default代码块
switch 变量{// 变量在switch中,case 后跟常量值
case 常量值:
// TODO
case 常量值1, 常量值2, ...:// 可以多值
// TODO
...
default:// 可忽略,至多出现一次
// TODO
}
switch {// switch中没有变量,case 后跟条件表达式
case 条件表达式:
// TODO
case 条件表达式:
// TODO
...
default:// 可忽略,至多出现一次
// TODO
}
type-switch 结构(类型断言结构)
- 接口变量为 nil常量值时,断言
switch 接口变量.(type) {
case 类型1:
// 变量是类型1时的处理
case 类型2:
// 变量是类型2时的处理
…
default:
// 变量不是所有case中列举的类型时的处理
}
switch tempValue := 接口变量.(type) {// 可以获取断言结果
case 类型1:
// 变量是类型1时的处理
case 类型2:
// 变量是类型2时的处理
…
default:
// 变量不是所有case中列举的类型时的处理
fmt.Printf("Unexpected type %T\n", tempValue)// 利用fmt包获取变量的信息:类型、值...
}
for…
for 初始化语句;条件表达式;结束语句 {// 三个语句都忽略时,for后面的分号";"也不用写
// TODO
}
跳转语句
goto LabelName
- LabelName 可以声明在任意语句前后
- LabelName声明与被调用的前后顺序可以不一样
break (LabelName) —— 跳出循环
- 可以跳出指定的循环
- LabelName 只能声明在流程控制结构声明前:switch、for、select
- LabelName被调用的位置必须在声明的循环内
continue (LabelName) —— 跳转至下一个循环
- 可以结束指定的本次循环,继续下一次循环
- LabelName 只能声明在流程控制结构声明前:for
- LabelName被调用的位置必须在声明的循环内
fallthrough
- 使用与switch的case代码块最后,声明继续执行下一个case\default代码块
- 为了兼容存在,不推荐使用
panic
return
包
fmt:格式化、打印输出
strconv:字符串和其他类型的相互转换
math:数值运算
unicode:字符集
bytes:字节切片,与strings包功能类似,I/O
flag:命令行参数解析
sort:排序与查找
regexp:正则表达式
encoding:对结构化信息的标准格式的编码解码协议
json
xml
asn1
bufio:I/O对象及操作
ReadWriter
Reader
Writer
http:网络请求
time:时间的获取与计算
runtime:异常、GC
log:日志
errors:异常
- 异常抛出会引起“宕机”,程序中断结束
- panic()后的语句不会被执行,但 panic()前的 defer语句会在异常抛出后、程序退出前执行
testing:测试
- 测试文件名,必须以“_test.go”为文件后缀名
- 测试函数,必须以“Test”或“Benchmark”为函数名前缀,运行时会直接执行测试函数,不需要用main函数作为程序入口
- 运行测试函数时,使用命令“go test”
1、单元测试
- Test_函数的形参:(t *testing.T)
2、性能(压力)测试
- Benchmark_函数的形参:(t *testing.B)
3、覆盖率测试
环境配置
下载sdk包:
地址:https://golang.google.cn/dl/
建议下载1.13以上,直接下最新的即可
配置环境变量:
# golang sdk包的根路径
GOROOT=%sdk的根路径%
# golang可执行文件的地址
PATH=%GOROOT%\bin
# golang项目代码存放路径
# 项目真实存放在 `%GOPATH%\src` 目录下
# go mod 拉取的依赖包会自动下载到 `%GOPATH%\pkg` 下
GOPATH=%golang项目代码的存放根路径%
# 公共代理镜像仓库。默认的中国无法访问
GOPROXY = "https://goproxy.io,direct"
# 模块支持,go 会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod 下载依赖。旧版gosdk只支持vendor下载依赖
GO111MODULE=on
常用命令
下载依赖
# 下载依赖。要存在 go.mod 文件。
go mod download
# 清理无用依赖,下载需要的依赖
go mod tidy
编译生成可执行文件 —— 支持跨平台编译(如windows机器上可以编译出linux版本的可执行文件)
go mod {main.go的路径}
# 指定编译生成的可执行文件的名称
go mod -o {nameWithSuffix_of_executable_file} main.go
# 编译linux x86-64格式的文件
go env -w GOOS=linux
go env -w GOARCH=amd64
go mod -o {nameWithSuffix_of_executable_file} main.go
# 编译linux arm格式的文件
go env -w GOOS=linux
go env -w GOARCH=arm64
go mod -o {nameWithSuffix_of_executable_file} main.go
# 编译windows x86-64格式的文件
go env -w GOOS=windows
go env -w GOARCH=amd64
go mod -o {nameWithSuffix_of_executable_file} main.go