Go 基本语法⚓︎
约 4891 个字 495 行代码 预计阅读时间 31 分钟
基础⚓︎
- 基本指令- go run test.go:编译 + 运行
- go build test.go:生成编译后的二进制文件- test,运行该文件还需再执行- ./test命令
 
- 
包:本质上是一个目录,里面包含一个或多个 .go 源程序,或者其他的包 - 如果某个包内的变量、函数等要被其他包引用,需要在命名时首字母大写,否则只能在包内(包括包内的其他文件,不需要 import)使用
- 包的类型:- Go 标准库自带的包
- 第三方包
- 项目内部的包
- 其他项目的包
 
- 
导入包: - 单个包:import "packageName"
- 多个包:import( "pack1", "path/to/pack2", ...)
- 
为包创建别名: - 如果 newName为一个.,那么后续无需再使用点表示法访问成员,前面不用跟包的名称了
 
- 如果 
 
- 单个包:
- 
包中的源码均以 package packageName开头,其中packageName表示导入路径的最后一个元素
 
- 如果某个包内的变量、函数等要被其他包引用,需要在命名时首字母大写,否则只能在包内(包括包内的其他文件,不需要 
变量⚓︎
- 
基本数据类型 int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 别名 rune // int32 别名,表示一个 Unicode 码位 float32 float64 complex64 complex128 // 复数 bool string- int、- uint、- uintptr在 32 bit 系统上为 32 bit,在 64 bit 系统上为 64 bit
- 平时应使用 int类整数,除非有特殊情况
- 复数- complex64:实部和虚部都是- float32类型的值
- complex128:实部和虚部都是- float64类型的值
- 虚部为 1 时,1 不可省略
 
- 字符串- 访问字符串:str[index]/for...range循环
- len(str):获取字符串长度
- 不能直接修改字符串的字符,除非对整个字符串重新赋值
- 也不能获取字符串某个字符的地址
- 字符串也有类似切片的操作(str[low: high]) ,但和切片不同之处在于:对截取的部分字符串的修改不会影响原字符串,而对部分切片的修改会改变原切片的值
- Cheat Sheet
 
- 访问字符串:
- reflect.Typeof(var)或在- fmt.Println()使用- %T占位符打印来查看变量- var的类型
- var语句用于声明一系列变量
- 
全局变量:函数外定义的变量,允许声明后不使用。有以下声明方法: // 法1 var name type = value // 法2(注意:这样声明的变量只能在函数内赋值,不能在全局范围内赋值) var name type // 法3 var name = value // 法4(不常用) var ( name1 type1 = value1 name2 type2 name3 = value3 // ... )- 
作用域:整个包,甚至可以作为外部包的成员用于其他程序中 - 局部变量:函数内定义的变量(包括函数的参数和返回值) ,声明后必须使用,否则编译报错。有以下使用方法:
 
- 局部变量:函数内定义的变量(包括函数的参数和返回值
- 
作用域:函数内部 
- 局部变量可以“隐藏”全局同名变量
- 零值:没有明确初始值的变量会自动赋予一个对应类型的零值- 数值类型:0(复数是 0 + 0i)
- 布尔型:false
- 字符串型:""
- 指针、切片、映射、函数、通道、接口:nil
- 结构体:每个字段都有对应类型的零值
 
- 类型转换 T(exp),将表达式exp的值转为类型T- 不同类型的变量赋值时需要这种显式类型转换
 
- 常量 const- 声明的同时必须赋值,之后值无法修改
- 不能用 :=声明
- 声明后可以不用(无论全局还是局部) ,不会报错
- 枚举
 
- iota:特殊常量,用于由常量构成的「枚举- 」 ,作为索引值(从 0 开始)
- 如果枚举中某个常量未赋值- 若它的前一个常量值是 iota,则它的值为该常量值 + 1
- 否则它的值等于前一个常量值
 
- 若它的前一个常量值是 
- 运算符- 算术运算符:+、-、*、/、%、++、--
- 关系运算符:==、!=、>、>=、<、<=
- 逻辑运算符:&&、||、!
- 位运算符:&、|、 (异或、取反均为该运算符) 、<<、>>、&()
- 赋值运算符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=
- 优先级:
 
 
- 
 
控制流⚓︎
所有控制语句的大括号都是必需的
条件语句⚓︎
- 
if判断- 类似 for循环:条件表达式可以不加括号,大括号是必需的
- 
可以在条件表达式前先执行一条简短的语句,该语句声明的变量作用域仅在 if语句之内
- 
else和if else语句同 C 语言
- 在 if简短语句声明的变量在所有分支中均可使用
- switch分支
- 与 C 语言的不同:- 和 if语句一样也可以在条件表达式前有一个简单的声明语句,作用域在switch语句内
- 可以省略 switch条件(同switch true)
- 只会执行其中一个 case分支,而不会继续执行后面的所有分支(相当于 C 语言中自动为每个分支加上break)- 如果想要继续执行后面的分支,可以在分支后面加 fallthrough关键词(当然后面的分支没有fallthrough的话就会在该分支停止)
 
- 如果想要继续执行后面的分支,可以在分支后面加 
- case的值无需是常量,且不限于整数
 
- 和 
 
- 类似 
注:没有三目运算符
?
循环语句⚓︎
- 
for循环- 由三部分构成:初始化语句、条件表达式、后置语句(类似去括号版的 C 语言 for循环)- 初始化语句和后置语句是可选的(分号可去掉)
 
- 
只保留条件表达式的循环可视为(C 语言的)while 循环 
- 
省略这三者会形成一个死循环 
- break:跳出当前循环,- continue:进入下一轮循环
- range迭代:适用于字符串、数组、切片、集合或通道
 
- 由三部分构成:初始化语句、条件表达式、后置语句(类似去括号版的 C 语言 
defer⚓︎
defer 推迟:用于函数或方法调用前,使该函数或方法在外层函数返回之后再执行
- 被 defer的函数的参数值在执行到defer语句时就被确定下来了
- 若函数内调用多次 defer,则在该函数返回前,按照 LIFO 原则,先defer的函数后执行,后defer的函数先执行
- 用途:用于成对的操作,比如文件的开关,锁的创建和释放等
- 如果函数因调用 os.Exit()退出,那里面的defer就不会执行了
函数⚓︎
- 函数定义:
func name([parameter list]) [return_type] {
    // ...
}
// 举例:
// 有四个参数,两个返回值
func name(a, b int, c, d string) (int, string) {
    // ...
}
- 所有源文件都需要有一个 main()函数作为主程序(且在开头声明package main) ,否则程序无法通过编译
- 当参数类型相同时,可以只保留最后一个类型(比如 x int, y int可简写为x, y int)
- 函数可以同时返回多个值(比如 return x, y) ,
- 
可以为返回值命名(介于函数名和参数列表之间 ) ,但是要同时为所有返回值都命名,部分命名会报错
- 
空函数: var f func(),它的值为nil,执行该函数会报错
- 函数传参只有值传递一种类型,没有引用传递!- 虽然形参可以是指针,能够改变传入的参数值,但它本质上是拷贝了该参数的地址,所以还是一种值传递
 
- 函数也可以像值一样传递,比如将函数赋给某个变量,作为其他函数的参数或返回值等
- 递归函数:同 C 语言
- 函数闭包:引用函数体之外的变量的函数(简单理解为“定义在一个函数内部的函数”,是一种匿名函数) ,这类函数被绑定在变量上,从而使变量的值始终保存在内存中
例子
- init函数- 无参数、无返回值
- init函数不能被显式调用,在- main函数执行前自动调用
- 一个包里可以有多个 init函数,调用顺序不确定
- 无论某个 init函数被多少个包导入,在程序中只调用一次
 
复杂类型⚓︎
指针⚓︎
- 
声明 
- 
其零值为 nil
- 
与指针相关的运算符 - 
&操作符:取变量的内存地址,即指向该变量的指针
- 
*操作符:解引用操作,即获取指针指向的底层值
 
- 
- 
指针数组 
- 
多重指针 
- 
与 C 语言不同的地方 - 数组名不是指向数组首元素的地址
- 指针没有算术运算
 
结构体⚓︎
结构体 struct:可看作一组字段
- 
声明和初始化 
- 
结构体指针 
- 
成员访问运算符 .- 不同于 C 语言,即使是指向结构体的指针也是用 .访问成员(隐式解引用)
 
- 不同于 C 语言,即使是指向结构体的指针也是用 
- 方法:为结构体定义方法,可以使结构体类似 C++ 的类
- 如果结构体要被外部包使用,那么该结构体及其成员的名称开头需大写
- 
标签 (tag):结构体字段后面可以跟一个可选的字符串,作为相应字段的属性,这被称为标签 - 一个标签可用于多个字段
- 
设置空标签和不使用标签的效果相同 
- 
使用 reflect包来访问结构体的标签
- 
相关方法(注意标签需要用双引号包裹,反引号包裹的标签无法使用) - 
Lookup()函数:返回两个值——与键关联的值和表示是否找到键的布尔值type T struct { f string `one:"1" two:"2"blank:""` } func main() { t := reflect.TypeOf(T{}) f, _ := t.FieldByName("f") fmt.Println(f.Tag) // one:"1" two:"2"blank:"" v, ok := f.Tag.Lookup("one") fmt.Printf("%s, %t\n", v, ok) // 1, true v, ok = f.Tag.Lookup("blank") fmt.Printf("%s, %t\n", v, ok) // , true v, ok = f.Tag.Lookup("five") fmt.Printf("%s, %t\n", v, ok) // , false }
- 
Get()函数:仅返回与键关联的值
 
- 
- 
将结构体转换为其他类型的结构体时要求底层类型相同,但在转换过程中会忽略掉标签 
 
数组、切片⚓︎
- 
声明: - 数组一旦声明,长度便固定下来
- 字面量:[n]T{x1, x2, ..., xn},其中长度n可以省略,x1到xn为 n 个T类型的值
- 可以不直接指出长度,用 ...替代n,由编译器自行推断
- 在数组长度已知的情况下,可以根据索引指定对应的元素值
 
- 
访问数组 - 下标法
- 
range遍历:用于for循环- 
每次迭代都会返回两个值,分别是索引和对应索引下的元素副本 
- 
可以使用空白标识符 _忽略不想获取的值
- 
如果只需要索引,可以直接忽略第二个变量 
 
- 
 
- 
len(s):获取数组长度
- 多维数组
- 数组作为参数- 形参必须指定长度([N]type) ,且实参的长度必须与形参相同,否则报错
- 若要改变数组内容,需要将数组指针作为参数(*[N]type) ,实参为数组的地址(&array)
- 如果没有指定长度([]type) ,那就是切片参数,不是数组啦
 
- 形参必须指定长度(
- 
切片:数组的一种抽象 - 
切片的底层数据结构 - 所以对切片的修改就是对底层数组的修改- 声明和初始化
 
 
- 所以对切片的修改就是对底层数组的修改
- 
零值为 nil,此时长度和容量均为 0 且没有底层数组
- 访问:同数组
- 截取(类似 Python)- a[low: high],获取索引值在- low到- high - 1之间的数组元素。截取到的切片有一个指向原数组的指针,所以修改切片也会修改该数组
- 可以省略切片的上下界,下界默认为 0,上界默认为数组长度
 
- 常用函数- len(s):获取切片长度
- cap(s):获取切片容量(从它的第一个元素开始,到其底层数组元素末尾的个数)
- append(s, x1, x2, ...)函数:向切片- s后面附加- x1等同类型的元素,返回值新添加元素后的切片- 如果加入元素太多超出容量,程序会分配一个更大的数组
- 只能用于切片,不能用于数组
 
- copy(dstSlice, srcSlice):将- srcSlice切片的元素拷贝到另一个切片- dstSlice内- 若 len(dstSlice) < len(srcSlice),则只会拷贝srcSlice中前len(dstSlice)个元素
- 若 len(dstSlice) == 0,那么不会拷贝任何元素
 
- 若 
 
- 多维切片:每一维的切片大小可以不同
- 切片作为参数- 切片传参有类似引用传递的效果——无需指针也可以在函数内修改切片的值(当然也会修改底层函数的值) (记住本质上还是值传递,只是因为切片有一个指向底层数组地址的指针)
- 但是如果在函数体内使用 append()为切片添加新元素,则不会改变外部切片的值
 
- 切片传参有类似引用传递的效果——无需指针也可以在函数内修改切片的值(当然也会修改底层函数的值
 
- 
映射⚓︎
映射 map:将键映射到值上,是一组无序的键值对(类似 Python 字典)
- 键必须支持 ==和!=比较,因此切片、函数、映射不能作为键
- 
声明和初始化 
- 
零值: nil,此时既没有键,也不能添加键
- 若映射的值的类型是一样的,那么可以在字面量的元素中省略它们(在上例中就是将两个键后面的 Vertex去掉)
- 插入 / 修改元素:m[key] = elem
- 获取元素:elem = m[key]
- 删除元素:delete(m, key)- 允许删除不存在的键,不会报错
 
- 检查某个键是否存在:elem, ok = m[key]- 若在 ok为true
- 否则为 false,此时elem为该类型的零值,且不会插入新的元素
- 如果 elem和ok在之前未声明,请使用:=短变量声明
 
- 若在 
- 元素个数:len(m)
- 映射作为函数形参,可以在函数体内改变外部实参的值
方法和接口⚓︎
方法⚓︎
方法:虽然 Go 没有类,但是可以为任意类型(一般是自定义类型或结构体)定义方法——这是一类带特殊的接收者(receiver) 参数的函数
- 
接收者位于 func和方法名之间,有一个自己的参数列表
- 
接收者的类型定义和方法声明必须位于同一个包内 
- 接收者参数可以是指针类型的,这样就可以在方法内修改接收者的值了。调用方法时接受者既可以是指针,也可以是值(此时会自动转化为指针(&x) )
- 接收者参数是一般的值时,调用方法时接收者也可以是一般值或指针(此时会自动转化为值(*p) )推荐用指针接收者参数:不仅可以修改接收者的值,而且避免拷贝占用大量内存 
- 将方法修改为一般函数:将接收者放入参数列表内即可
接口⚓︎
接口 (interface):可理解为一组仅包含方法的集合
- 
声明(隐式实现 ) :接口的实现(方法)可以出现在任何包内,且无需在每个实现上增加新的接口名称package main import ( "fmt" "math" ) type Abser interface { Abs() float64 } func main(){ var a Abser f := MyFloat(-math.Sqrt2) // v := Vertex{3, 4} a = f // a = &v fmt.Println(a.Abs()) } type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) } type Vertex struct { X, Y float64 } func (v *Vertex) Abs() float64{ return math.Sqrt(v.X * v.X + v.Y * v.Y) }
- 
多个类型可以共用一个接口,一个类型可以使用多个接口 
- 接口可嵌套
- 接口也是值,可以像值一样传递,作为函数的参数或返回值。具体来说,接口值保存了包含值和类型的元组 (value, type)- 不要误会,还是得把接口值看作单个值,只是它有两种表现形式,既可以表示底层类型的值,也可以表示当前所指类型
- 如果底层值为 nil(接口自身不为nil) ,方法仍然会被nil接收者调用而不会报错
- 如果接口自身为 nil,那么它既不保存值也不保存类型,这会产生运行时错误
 
- 空接口:没有指明方法的接口 interface{},可保存任何类型的值,因此可以用来存储未知类型的值
- 
类型断言 (type assertion):用于访问接口的底层值 - t := i.(T):接口- i保存了类型为- T的底层值(若类型不对就会报错- ) ,将其赋给- t
- t, ok := i.(T):用- ok检查- i是否保存类型为- T的底层值,若是- ok为- true,否则为- false,但是此时不会报错
- 类型选择 (type switches):根据类型断言来选择分支,类似 switch语句(此时的case为类型而不是值)
 
- 
常用接口 - fmt包的- Stringer接口,定义了将其他类型转为字符串的方法
 例子package main import "fmt" type Person struct { Name string Age int } func (p Person) String() string { return fmt.Sprintf("%v (%v years)", p.Name, p.Age) } func main() { a := Person{"Arthur Dent", 42} z := Person{"Zaphod Beeblebrox", 9001} fmt.Println(a, z) } // Output: // Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)- error接口:定义了处理错误情况的方法。通常函数会返回一个- error值,如果值为- nil表示成功,否则表示失败,需要有对应的错误处理
 例子package main import ( "fmt" "time" ) type MyError struct { When time.Time What string } func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What) } func run() error { return &MyError{ time.Now(), "it didn't work", } } func main() { if err := run(); err != nil { fmt.Println(err) } } // Output: // at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work- io包的- Reader接口,表示数据流的读取端- io.Reader接口有一个- Read方法
 
 例子package main import ( "fmt" "io" "strings" ) func main() { r := strings.NewReader("Hello, Reader!") b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n = %v err = %v b = %v\n", n, err, b) fmt.Printf("b[:n] = %q\n", b[:n]) if err == io.EOF { break } } } // Output(每次读取 8 Byte 信息): // n = 8 err = <nil> b = [72 101 108 108 111 44 32 82] // b[:n] = "Hello, R" // n = 6 err = <nil> b = [101 97 100 101 114 33 32 82] // b[:n] = "eader!" // n = 0 err = EOF b = [101 97 100 101 114 33 32 82] // b[:n] = ""
泛型⚓︎
- 
类型参数 (type parameters) - 类型参数列表:[P, Q constraint1, R constraint2]- 其中 P, Q, R都是类型参数,constraint1, constraint2都是类型限制
- 类型参数列表介于函数名和参数列表之间
- 不能用于方法,只能用于函数
 
- 其中 
 例子- 实例化 (instantiation):在泛型函数的基础上生成一个非泛型函数,用于真正的函数执行,实现过程如下:- 把泛型函数的类型参数替换为类型实参(比如将类型参数 T替换为int)
- 检查类型实参是否满足泛型函数定义的类型限制
- 若任何一步失败,泛型函数调用失败
 
- 把泛型函数的类型参数替换为类型实参(比如将类型参数 
- 类型参数除了用于泛型函数外,还用于创建泛型类型 (generic types)
 
- 类型参数列表:
- 
类型集 (type sets):类型参数的类型限制包含多个具体类型,这些具体类型构成了类型集,泛型函数只支持这些类型 - 
举例:类型限制 constraints.Ordered包含如下具体类型:
- 
类型限制必须是 interface类型
- 
类型限制相关的符号 - |:取并集(上面的例子中就用到了)
- ~T:表示底层类型是- T的所有类型
 
- 
类型限制字面量:可以直接在类型限制列表里现场定义类型限制 - interface{E}可简写为- E
- any可作为- interface{}的别名,表示支持任意类型
 
 
- 
- 
类型推导 (type inference):在调用泛型函数时,可以不指定(全部或部分)类型实参,由编译器根据传入的函数实参(或部分已知的类型参数)来推导出类型实参,这样使代码更简洁 func min[T constraints.Ordered] (x, y T) T { if x < y { return x } return y } var a, b, m1, m2 float64 // 不指定类型参数,让编译器进行类型推导 m2 = min(a, b)- 类型推导不一定成功,比如类型参数用于函数的返回值,或者用于别的函数内
 
何时用泛型
- slice、- map、- channel里的元素类型较多
- 设计通用的数据结构,比如链表、二叉树等
- 当一个方法的实现逻辑对所有类型都一样时
并发编程⚓︎
goroutine、通道⚓︎
- 
goroutine:一种轻量级的用户态线程,实现并发编程 - 语法:go后面跟函数调用,这样为该函数启用一个 goroutine
- Go 为 main() 函数创建一个默认的 goroutine。如果 main() 函数运行结束,则所有在 main() 中启动的 goroutine 会立马结束
 例子package main import "fmt" func hello() { fmt.Println("hello") } func main() { go hello() fmt.Println("main end") }有以下几种可能的输出结果: - main end
- main end- hello
- hello- main end
 因为 main() 函数的 goroutine 和 hello()函数的 goroutine 是并发执行的,所以谁先谁后都有可能- goroutine 和闭包一起使用时需注意:避免多个 goroutine 闭包使用同一个变量- 可以为每个 goroutine 声明一个新变量来解决这一问题
 
 
- 语法:
- 
通道 (channel):作为多个 goroutine 通信的“桥梁”,一个 goroutine 可以发送数据到指定通道,其他 goroutine 可以从该通道获取数据 - 类似队列,满足 FIFO 原则
- 零值为 nil,值为零值的通道不能用于通信
- 
声明和初始化 
- 
向通道发送值: channel_name <- value
- 从通道接收值:value = <-channel_name
- 关闭通道:close(channel_name)- 关闭空通道会报错
 
- 通道缓冲区:用 make函数声明时可以指定缓冲区容量,要考虑阻塞问题- 可用 cap(channel_name)获取容量大小
- 无缓冲区- 向通道发送值时,必须确保其他通道会从该通道接收值,发送才能成功
- 从通道接收值时,必须确保其他通道会向该通道发送值,接收才能成功
 
- 有缓冲区- 若缓冲区未满,那么发送方发送数据到通道缓冲区后,便可以继续往下执行,而无需等待接收方从通道接收数据
- 若缓冲区已满,那么发送方发送数据到通道缓冲区后会阻塞,直到接收方从通道接收数据,缓冲区有空间存储发送方发送的数据时,发送发才能继续往下执行
 
 
- 可用 
- 遍历通道(for...range)- 死循环读取通道- 若通道已关闭,那么继续从通道获取的值是对应类型的零值
- 若通道未关闭,则会遇到阻塞报错
 
 
- 死循环读取通道
 例子package main import "fmt" import "time" func addData(ch chan int) { /* 每3秒往通道ch里发送一次数据 */ size := cap(ch) for i:=0; i<size; i++ { ch <- i time.Sleep(3*time.Second) } // 数据发送完毕,关闭通道 close(ch) } func main() { ch := make(chan int, 10) // 开启一个goroutine,用于往通道ch里发送数据 go addData(ch) /* range迭代从通道ch里获取数据 通道close后,range迭代取完通道里的值后,循环会自动结束 */ for i := range ch { fmt.Println(i) } }- 通道作为函数形参时,可以控制数据和通道之间的数据流向- 只读(仅可以从通道读取数据) :<- chan type
- 只写(仅可以向通道写入数据) :chan <- type
 
- 只读(仅可以从通道读取数据
 
错误处理⚓︎
Go 用两套机制区分错误(error) 和异常(panic)
- 
错误:以返回值的形式返回 - 
通过多返回值的方式处理调用函数时发生的错误 
- 
也可以自定义方法来处理错误 
 
- 
- 
异常:导致终止程序 - panic()函数:调用该函数直接抛出异常- 参数值可以是数字、字符串、函数
- 如果参数是函数 F,那么会有以下行为- 执行 F 中被 defer的函数
- 如果 F 有上一级函数 E,E 也被视为异常,因此执行 E 中被 defer的函数
- 重复上一步,直至没有上一级函数
- 程序终止
 
- 执行 F 中被 
 
- 
recover()函数:用于捕获异常,必须结合defer才能生效- 如果当前 goroutine 出现异常,可以在代码适当位置调用 recover(),使程序继续正常执行而不停止
- 函数返回 nil的情况:- 没有异常发生
- panic()函数的参数为- nil
- recover()函数不是在被- defer的函数里面被直接调用执行的
 
 例子package main import ( "fmt" ) func a() { defer func() { /*捕获函数a内部的panic*/ r := recover() fmt.Println("panic recover", r) }() panic(1) } func main() { defer func() { /*因为函数a的panic已经被函数a内部的recover捕获了 所以main里的recover捕获不到异常,r的值是nil*/ r := recover() fmt.Println("main recover", r) }() a() fmt.Println("main") } // Output: // panic recover 1 // main // main recover <nil>
- 如果当前 goroutine 出现异常,可以在代码适当位置调用 
 
后端库相关⚓︎
其他⚓︎
go get 命令相关⚓︎
执行 go get 之前的准备工作:
- 配置 Go 的代理服务器(不配置的话很容易出现连接失败的问题) :前往这个网站,按照步骤输入一些命令。之后用go env查看环境变量,若GOPROXY的值为该网址,说明配置成功。
- 在源代码的同一目录中执行 go mod init xxx(xxx名称任意) ,创建一份 .mod 文件
做好这些准备工作后,再执行 go get 命令获取 Go 在线资源。
评论区