Golang
Go的基本概念
包
在 Go 中,程序是通过将包链接在一起来构建的。Go 中进行导入的最基本单位是一个包,而不是.go文件。包其实就是一个文件夹,英文名 package,包内共享所有变量,常量,以及所有定义的类型。包的命名风格建议都是小写字母,并且要尽量简短。
可见性
Go并没有public,pravite等关键字,所以Go提供了另外一种简洁的方式去实现可见性,它控制可见性的方式非常简单,规则如下
- 名称大写字母开头,即为公有类型/变量/常量
- 名字小写或下划线开头,即为私有类型/变量/常量
1 | |
导入包
- 导入一个包就是导入这个包的所有公有的类型/变量/常量,导入的语法就是import加上包名
1
2
3
4
5
6package main
import (
"example"
"example1"
) - 如果有包名重复了,或者包名比较复杂,你也可以给它们起别名
1
2
3
4
5
6package main
import (
e "example"
e1 "example1"
) - 匿名导入包
匿名导入的包无法被使用,这么做通常是为了加载包下的init函数,但又不需要用到包中的类型,例如一个常见的场景就是注册数据库驱动1
2
3
4
5
6package main
import (
e "example"
_ "mysql-driver"
)Go 中无法进行循环导入,不管是直接的还是间接的。例如包 A 导入了包 B,包 B 也导入了包 A,这是直接循环导入,包 A 导入了包 C,包 C 导入了包 B,包 B 又导入了包 A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。
- 内部包
go 中约定,一个包内名为internal 包为内部包,外部包将无法访问内部包中的任何内容,否则的话编译不通过
运算符
- go 语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。go 也支持增强赋值运算符,如下
1
2
3a += 1
a /= 2
a &^= 2在 Go 语言中,& 和 ^ 是两个不同的运算符,它们可以组合成 &^,具有特定的意义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
171. ^ 是 按位取反 (^) 的含义:
- 对每个位取反:0 变成 1,1 变成 0。
- 适用于整数类型(如 int, uint 等)
2. &^ 是一个位操作符,用于 按位清除。它对左操作数的位值执行清除操作,根据右操作数的对应位来决定是否清除,广泛用于位操作场景。
package main
import "fmt"
func main() {
a := 6 // 二进制: 0110
b := 3 // 二进制: 0011
result := a &^ b
fmt.Println("Result:", result) // 结果: 4 (二进制: 0100)
}
Go 语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++和++i这样的问题。
- a++ // 正确
- ++a // 错误
- a— // 正确
还有一点就是,它们不再具有返回值,因此a = b++这类语句的写法是错误的。
字面量
编程语言源程序中表示固定值的符号叫做字面量,也称字面常量。一般使用裸字符序列来表示不同类型的值。字面量可以被编程语言编译器直接转换为某个类型的值。Go的字面量可以出现在两个地方:一是用于常量和变量的初始化,二是用在表达式中作为函数调用实参。变量初始化语句中如果没有显式地指定变量类型,则Go编译器会结合字面量的值自动进行类型推断。Go中的字面量只能表达基本类型的值,Go不支持用户自定义字面量。
风格
Go 官方提供了一个格式化工具gofmt,通过命令行就可以使用,该格式化工具没有任何的格式化参数可以传递,仅有的两个参数也只是输出格式化过程,所以完全不支持自定义,也就是说所有通过此工具的格式化后的代码都是同一种代码风格,这会极大的降低维护人员的心智负担,所以在这一块追求个性显然是一个不太明智的选择。
函数花括号换行
在 Go 中所有的花括号都不应该换行
1 | |
代码间隔
Go 中大部分间隔都是有意义的,从某种程度上来说,这也代表了编译器是如何看待你的代码的,例如下方的数学运算
29 + 1/32
众所周知,乘法的优先级比加法要高,在格式化后,*符号之间的间隔会显得更紧凑,意味着优先进行运算,而+符号附近的间隔则较大,代表着较后进行运算
花括号不可省略
在其它语言中的 if 和 for 语句通常可以简写,像下面这样
1 | |
但在 Go 中不行,你可以只写一行,但必须加上花括号
1 | |
变量
在 go 中的类型声明是后置的,变量的声明会用到var关键字,格式为var 变量名 类型名,变量名的命名规则必须遵守标识符的命名规则。
例如
1 | |
当要声明多个相同类型的变量时,可以只写一次类型
1 | |
当要声明多个不同类型的变量时,可以使用()进行包裹,可以存在多个()。
1 | |
变量赋值
- 官方提供的语法糖:短变量初始化,可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断
1
name := "jack" // 字符串类型的变量。 - 短变量声明方式无法对一个已存在的变量使用,比如
1
2
3
4
5
6
7// 错误示例
var a int
a := 1
// 错误示例
a := 1
a := 2 但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如
1
2a := 1
a, b := 2, 2在 go 语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它
1
2
3
4
5func main() {
a := 1
}
// 报错:a declared and not used这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译
1
2
3
4
5var a = 1
func main() {
}匿名
用下划线可以表示不需要某一个变量
1
2
3
4//Open有两个变量
Open(name string) (*File, error)
//比如os.Open函数有两个返回值,我们只想要第一个,不想要第二个,可以按照下面这样写
file, _ := os.Open("readme.txt")交换
在 Go 中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下
1
2num1, num2, num3 := 25, 36, 49
num1, num2, num3 = num3, num2, num1
代码块
在函数内部,可以通过花括号建立一个代码块,代码块彼此之间的变量作用域是相互独立的。例如下面的代码
1 | |
数组
如果事先就知道了要存放数据的长度,且后续使用中不会有扩容的需求,就可以考虑使用数组,Go 中的数组是值类型,而非引用,并不是指向头部元素的指针。
还可以通过内置函数len来访问数组元素的数量 len(nums)
内置函数cap来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义。 cap(nums)
切割
切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开,数组在切割后,就会变为切片类型,例子如下
1 | |
切片
切片的初始化方式有以下几种
1 | |
可以看到切片与数组在外貌上的区别,仅仅只是少了一个初始化长度。通常情况下,推荐使用make来创建一个空切片,只是对于切片而言,make函数接收三个参数:类型,长度,容量。举个例子解释一下长度与容量的区别,假设有一桶水,水并不是满的,桶的高度就是桶的容量,代表着总共能装多少高度的水,而桶中水的高度就是代表着长度,水的高度一定小于等于桶的高度,否则水就溢出来了。所以,切片的长度代表着切片中元素的个数,切片的容量代表着切片总共能装多少个元素,切片与数组最大的区别在于切片的容量会自动扩张,而数组不会。
切片可以通过append函数实现许多操作,函数签名如下,slice是要添加元素的目标切片,elems是待添加的元素,返回值是添加后的切片。
- 插入数据
- 尾部插入数据新 slice 预留的 buffer 容量 大小是有一定规律的。 在 golang1.18 版本更新之前网上大多数的文章都是这样描述 slice 的扩容策略的: 当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的 1.25 倍。 在 1.18 版本更新之后,slice 的扩容策略变为了: 当原 slice 容量(oldcap)小于 256 的时候,新 slice(newcap)容量为原来的 2 倍;原 slice 容量超过 256,新 slice 容量 newcap = oldcap+(oldcap+3*256)/4
1
2
3nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 可以看到长度与容量并不一致。
- 尾部插入数据
- 头部插入数据
1
2nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10] - 从中间下标 i 插入元素
1
2nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]
- 删除元素
切片元素的删除需要结合append函数来使用,现有如下切片1
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
- 从头部删除 n 个元素
1
2nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10] - 从尾部删除 n 个元素
1
2nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7] - 从中间指定下标 i 位置开始删除 n 个元素
1
2nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10] - 删除所有元素
1
2nums = nums[:0]
fmt.Println(nums) // []多位切片
先来看下面的一个例子:可以看到,同样是二维的数组和切片,其内部结构是不一样的。数组在初始化时,其一维和二维的长度早已固定,而切片的长度是不固定的,切片中的每一个切片长度都可能是不相同的,所以必须要单独初始化,切片初始化部分修改为如下代码即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var nums [5][5]int
for _, num := range nums {
fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
fmt.Println(slice)
}
// 结果
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[]
[]
[]
[]
[]1
2
3
4
5
6
7
8
9
10slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
slices[i] = make([]int, 5)
}
// 结果
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]拓展表达式
切片与数组都可以使用简单表达式来进行切割,但是拓展表达式只有切片能够使用,该特性于 Go1.2 版本添加,主要是为了解决切片共享底层数组的读写问题,主要格式为如:slice[low:high:max] 需要满足关系low<= high <= max <= cap,使用拓展表达式切割的切片容量为max-low。
例如下方的例子中省略了max,那么s2的容量就是cap(s1)-low,s2的长度则为4-3=1.索引从3(包括)开始,到4(不包括)结束那么这么做就会有一个明显的问题,s1与s2是共享的同一个底层数组,在对s2进行读写时,有可能会影响的s1的数据。拓展表达式就是为了解决此类问题而生的,只需要稍微修改一下就能解决该问题1
2
3
4
5
6
7
8
9s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6
// 添加新元素,由于容量为6.所以没有扩容,直接修改底层数组
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)
//结果
[4 1]
[1 2 3 4 1 6 7 8 9]1
2
3
4
5
6
7
8
9
10
11func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4:4] // cap = 4 - 3 = 1
// 容量不足,分配新的底层数组
s2 = append(s2, 1)
fmt.Println(s2)
fmt.Println(s1)
}
//结果
[4 1]
[1 2 3 4 5 6 7 8 9]map
初始化一个 map 有两种方法,第一种是字面量,第二种方法是使用内置函数make,对于 map 而言,接收两个参数,分别是类型与初始容量,map 是引用类型,零值或未初始化的 map 可以访问,但是无法存放元素,所以必须要为其分配内存。例子如下:在 Go 中,map 的键类型必须是可比较的,比如string ,int是可比较的,而[]int是不可比较的,也就无法作为 map 的键
1
2
3mp := make(map[string]int, 8)
mp := make(map[string][]int, 10)map的访问
访问一个 map 的方式就像通过索引访问一个数组一样通过代码可以观察到,即使 map 中不存在”f”这一键值对,但依旧有返回值。map 对于不存的键其返回值是对应类型的零值,并且在访问 map 的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp["a"])
fmt.Println(mp["b"])
fmt.Println(mp["d"])
fmt.Println(mp["f"])
}
// 结果
0
1
3
01
2
3
4
5
6
7
8
9
10
11
12
13func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["f"]; exist {
fmt.Println(val)
} else {
fmt.Println("key不存在")
}
}函数
声明函数有两种办法,一种是通过func关键字直接声明,另一种就是通过var关键字来声明,如下所示1
2
3
4
5
6
7func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}Go语言中不支持函数重载
函数返回值
- 当函数没有返回值时,不需要void,不带返回值即可。
- Go 允许函数有多个返回值,此时就需要用括号将返回值围起来。
1
2
3
4
5
6func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0不能作为被除数")
}
return a / b, nil
} - Go 也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。
1
2
3
4
5func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}return关键字的优先级最高
函数的延时调用
defer关键字可以使得一个函数延迟一段时间调用,在函数返回之前这些 defer 描述的函数最后都会被逐个执行,看下面一个例子:当有多个 defer 描述的函数时,就会像栈一样先进后出的顺序执行1
2
3
4
5
6
7
8
9
10
11
12
13func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}
//结果
2
11
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}
//结果
0
2
5
4
3
1函数的参数预计算
对于延迟调用有一些反直觉的细节,比如下面这个例子这个坑还是比较隐晦的,可以猜一下结果1
2
3
4
5
6
7
8
9func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}从这里我们不难看出defer只是延时了fmt.Println()函数,但是并不会影响Fn1()函数,这称为参数预计算[结果]
2
3
1




