这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
前期准备
- 项目初始化:go mod init 项目名
- 开启go_modules:go env -w GO111MODULE=on go env -w GO111MODULE=auto
- 设置代理:go env -w GOPROXY=goproxy.cn,direct
- 下载依赖:go get 依赖
基本知识
变量,方法,常量等首字母大写,则为public类型
变量:
//变量命名;变量在声明之后,系统会自动将变量值初始化为对应类型的零值,比如 v1 的值为 0,v2 的值空字符串
var name string
var age int
//也可以将若干个需要声明的变量放置在一起
var (
v1 int
v2 string*
)
//变量初始化
var v1 int = 10 // 方式一,常规的初始化操作
var v2 = 10 // 方式二,此时变量类型会被编译器自动推导出来
v3 := 10 // 方式三,可以省略var 编译器可以自动推导出v3的类型;出现在 := 运算符左侧的变量应该是未声明过的,否则会导致编译错误
//数据类型
var v1 int // 整型
var v2 string // 字符串
var v3 bool // 布尔型
var v4 [10]int // 数组,数组元素类型为整型
var v5 struct { // 结构体,成员变量 f 的类型为64位浮点型
f float64
}
var v6 *int // 指针,指向整型
var v7 map[string]int // map(字典),key为字符串类型,value为整型
var v8 func(a int) int // 函数,参数类型为整型,返回值类型为整型
Go 是强类型语言,变量类型一旦确定,就不能将其他类型的值赋值给该变量,因此,布尔类型不能接受其他类型的赋值,也不支持自动或强制的类型转换;此外,由于强类型的缘故,Go 语言在进行布尔值真假判断时,对值的类型有严格限制,不同类型的值不能使用
==或!=运算符进行比较,在编译期就会报错
多重赋值功能
//交换 i 和 j 变量
i, j = j, i
匿名变量
//通过下划线 `_` 声明
func GetName() (userName, nickName string) {
return "nonfu", "学院君"
}
//若只想获得 nickName,则函数调用语句可以用如下方式实现
_, nickName := GetName()
尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。
常量
在 Go 语言中,常量是指编译期间就已知且不可改变的值,常量只可以是数值类型(包括整型、 浮点型和复数类型)、布尔类型、字符串类型等标量类型;通过 const 关键字定义常量时,可以指定常量类型,也可以省略(底层会自动推导)
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点常量
const ( // 通过一个 const 关键字定义多个常量,和 var 类似
size int64 = 1024
eof = -1 // 无类型整型常量
)
const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重赋值
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量
由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式(包括调用函数,方法)
预定义常量
Go 语言预定义了这些常量:true、false 和 iota。
iota 比较特殊,可以被认为是一个可被编译器修改的常量,在每一个 const 关键字出现时被重置为 0,然后在下一个 const 出现之前,每出现一次 iota,其所代表的数字会自动增 1。
package main
const ( // iota 被重置为 0
c0 = iota // c0 = 0
c1 = iota // c1 = 1
c2 = iota // c2 = 2
)
const (
u = iota * 2; // u = 0
v = iota * 2; // v = 2
w = iota * 2; // w = 4
)
const x = iota; // x = 0
const y = iota; // y = 0
如果两个 const 的赋值语句的表达式是一样的,那么还可以省略后一个赋值表达式。因此,上面的前两个 const 语句可简写为:
const (
c0 = iota
c1
c2
)
const (
u = iota * 2
v
w
)
复合类型
- 指针(pointer)
- 数组(array)
- 切片(slice)
- 字典(map)
- 通道(chan)
- 结构体(struct)
- 接口(interface)
运算符
-
支持
i++i--但不支持--i++i -
支持
+=、-=、*=、/=、%=这种快捷写法 -
由于Go 是强类型语言,只有同类型的值才能放在一起运算
-
各种类型的整型变量都可以直接与字面常量进行比较,如
intValue1 == 8 -
位运算符
运算符 含义 结果 x & y按位与 把 x 和 y 都为 1 的位设为 1 `x \ y` 按位或 x ^ y按位异或 把 x 和 y 一个为 1 一个为 0 的位设为 1 ^x按位取反 把 x 中为 0 的位设为 1,为 1 的位设为 0 x << y左移 把 x 中的位向左移动 y 次,每次移动相当于乘以 2 x >> y右移 把 x 中的位向右移动 y 次,每次移动相当于除以 2 -
逻辑运算符
运算符 含义 结果 x && y逻辑与运算符(AND) 如果 x 和 y 都是 true,则结果为 true,否则结果为 false `x \ y` 逻辑或运算符(OR) !x逻辑非运算符(NOT) 如果 x 为 true,则结果为 false,否则结果为 true -
运算符优先级
6 ^(按位取反) ! 5 * / % << >> & &^ 4 + - | ^(按位异或) 3 == != < <= > >= 2 && 1 ||
浮点数
Go 语言中的浮点数采用IEEE-754 标准的表达方式,定义了两个类型:float32 和 float64,其中 float32 是单精度浮点数,可以精确到小数点后 7 位(类似 PHP、Java 等语言的 float 类型),float64 是双精度浮点数,可以精确到小数点后 15 位(类似 PHP、Java 等语言的 double 类型)。
在实际开发中,应该尽可能地使用
float64类型,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。
字符串
var str string // 声明字符串变量
str = "Hello World" // 变量初始化
str2 := "你好,学院君" // 也可以同时进行声明和初始化
在 Go 语言中,字符串是一种基本类型,默认是通过 UTF-8 编码的字符序列,当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节,比如中文编码通常需要 3 个字节。
-
格式化输出
fmt.Printf("The length of "%s" is %d \n", str, len(str)) -
字符串操作:连接,切片,遍历
-
多行字符串
//使用 ` var str string // 声明字符串变量 str = "Hello World" // 变量初始化 str2 := "你好,学院君" // 也可以同时进行声明和初始化 //或者使用 + results := "Search results for "Golang":\n" + "- Go\n" + "- Golang\n" + "- Golang Programming\n" //输出结果 Search results for "Golang": - Go - Golang - Golang Programming
字符串是一种不可变值类型,一旦初始化之后,它的内容不能被修改
转义字符
\n:换行符\r:回车符\t:tab 键\u或 \U :Unicode 字符\:反斜杠或自身
基本数据类型之间的转化
数组
-
数组的定义
var a [8]byte // 长度为8的数组,每个元素为一个字节 var b [3][3]int // 二维数组(9宫格) var c [3][3][3]float64 // 三维数组(立体的9宫格) var d = [3]int{1, 2, 3} // 声明时初始化 var e = new([3]string) // 通过 new 初始化 //此外,还可以通过这种语法糖省略数组长度的声明,这种情况下,Go 会在编译期自动计算出数组长度。 a := [...]int{1, 2, 3} //数组在初始化的时候,如果没有填满,则空位会通过对应的元素类型零值填充 -
数组的遍历
-
使用for
-
Go 语言还提供了一个关键字
range,用于以更优雅的方式遍历数组中的元素,range表达式返回两个值,第一个是数组下标索引值,第二个是索引对应数组元素值for i, v := range arr { fmt.Println("Element", i, "of arr is", v) } //如果我们不想获取索引值 for _, v := range arr { // ... } //如果只想获取索引值 for i := range arr { // ... }
-
切片(golang中新的数据类型)
-
在 Go 语言中,切片是一个新的数据类型,与数组最大的不同在于,切片的类型字面量中只有元素的类型,没有长度, 切片的长度可以随着元素数量的增长而增长(但不会随着元素数量的减少而减少)
var slice []string = []string{"a", "b", "c"} -
基于数组创建
(切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的切片)
// 先定义一个数组 months := [...]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} // 基于数组创建切片 q2 := months[3:6] // 第二季度 summer := months[5:8] // 夏季 fmt.Println(q2) fmt.Println(summer)和字符串切片一样,这也是个左闭右开 [ , )的集合
-
基于
months的所有元素创建切片(全年) -
all := months[:] -
基于
months的前 6 个元素创建切片(上半年) -
firsthalf := months[:6] -
基于从第 6 个元素开始的后续元素创建切片(下半年)
-
secondhalf := months[6:] -
基于切片
-
q1 := firsthalf[:3]` // 基于 firsthalf 的前 3 个元素构建新切片
-
直接创建
Go: 语言提供的内置函数
make()可以用于灵活地创建切片//要创建一个初始长度为 5 的整型切片,可以这么做: mySlice1 := make([]int, 5) //要创建一个初始长度为 5、容量为 10 的整型切片,可以这么做(通过第三个参数设置容量): mySlice2 := make([]int, 5, 10) //此外,还可以直接创建并初始化包含 5 个元素的数组切片(长度和容量均为5): mySlice3 := []int{1, 2, 3, 4, 5} //和数组一样,所有未初始化的切片,会填充元素类型对应的零值。 -
动态增加元素
切片比数组更强大之处在于支持动态增加元素,甚至可以在容量不足的情况下自动扩容。
通常一个切片的长度值小于等于其容量值,我们可以通过 Go 语言内置的
cap()函数和len()函数来获取某个切片的容量和实际长度可以通过
append()函数向切片追加新元素var oldSlice = make([]int, 5, 10) newSlice := append(oldSlice, 1, 2, 3) //函数 append() 的第二个参数是一个不定参数,我们可以按自己需求添加若干个元素(大于等于 1 个) //可以直接将一个切片追加到另一个切片的末尾 appendSlice := []int{1, 2, 3, 4, 5} newSlice := append(oldSlice, appendSlice...) // 注意末尾的 ... 不能省略 -
内容复制
切片类型还支持 Go 语言的另一个内置函数
copy(),用于将元素从一个切片复制到另一个切片。如果两个切片不一样大,就会按其中较小的那个切片的元素个数进行复制。slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{5, 4, 3} // 复制 slice1 到 slice 2 copy(slice2, slice1) // 只会复制 slice1 的前3个元素到 slice2 中 // slice2 结果: [1, 2, 3] // 复制 slice2 到 slice 1 copy(slice1, slice2) // 只会复制 slice2 的 3 个元素到 slice1 的前 3 个位置 // slice1 结果:[5, 4, 3, 4, 5] -
动态删除元素
其实是通过切片的切片实现的「伪删除」,也可以用append()和copy()实现
slice3 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} slice3 = slice3[:len(slice3) - 5] // 删除 slice3 尾部 5 个元素 slice3 = slice3[5:] // 删除 slice3 头部 5 个元素 //输出结果 slice1: [0 2 0 0 0] slice2: [0 6] -
数据共享
切片底层是基于数组实现的;若基于
slice1创建slice2,则它们的数组指针指向了同一个数组,因此,修改slice2元素会同步到slice1,因为修改的是同一份内存数据,这就是数据共享问题解决数据共享问题
slice1 := make([]int, 4) slice2 := slice1[1:3] slice1 = append(slice1, 0) slice1[1] = 2 slice2[1] = 6 fmt.Println("slice1:", slice1) fmt.Println("slice2:", slice2) //输出结果 slice1: [0 2 0 0 0] slice2: [0 6]虽然
slice2是基于slice1创建的,但是修改slice2不会再同步到slice1,因为append函数会重新分配新的内存,然后将结果赋值给slice1,这样一来,slice2会和老的slice1共享同一个底层数组内存,不再和新的slice1共享内存,也就不存在数据共享问题了。但是这里有个需要注意的地方,就是一定要重新分配内存空间,如果没有重新分配,依然存在数据共享问题:
slice1 := make([]int, 4, 5) //!设定容量为5 slice2 := slice1[1:3] slice1 = append(slice1, 0) slice1[1] = 2 slice2[1] = 6 fmt.Println("slice1:", slice1) fmt.Println("slice2:", slice2) //输出结果 slice1: [0 2 6 0 0] slice2: [2 6]
字典
所谓字典,其实就是存储键值对映射关系的集合,和 Redis 一样,Go 字典也是个无序集合,底层不会按照元素添加顺序维护元素的存储顺序。
字典声明
- 字典的声明基本上没有多余的元素,比如:
//先声明再初始化
var testMap map[string]int
//声明和初始化合并
testMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
//前面我们提到 Go 字典是个无序集合,所以如果我们通过 fmt.Println(testMap) 打印 testMap 的值,得到的可能是下面这样的结果:
map[one:1 three:3 two:2]
其中,
testMap是声明的字典变量名,string是键的类型,int则是其中所存放的值类型。
-
此外,还可以像切片那样,通过 Go 语言内置的函数
make()来初始化一个新字典:var testMap = make(map[string]int)通过这种方式初始化后,可以往字典中添加键值对(前面那种声明方式不能这么操作,否则编译期间会抛出 panic):
testMap["one"] = 1testMap["two"] = 2testMap["three"] = 3 -
还可以通过
make函数的第二个参数选择是否在创建时指定该字典的初始存储容量(超出会自动扩容):
testMap = make(map[string]int, 100)
查找字典元素
value, ok := testMap["one"]
if ok { // 找到了
// 处理找到的value
}
从字典中查找指定键时,会返回两个值,第一个是真正返回的键值,第二个是是否找到的标识,判断是否在字典中成功找到指定的键,不需要检查取到的值是否为 nil,只需查看第二个返回值ok,这是一个布尔值,如果查找成功,返回 true,否则返回 false,配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。
在声明字典的键类型时要求数据类型必须是支持通过
==或!=进行判等操作的类型,比如数字类型、字符串类型、数组类型、结构体类型等
删除字典元素
delete(testMap, "four")
指针
a := 100
//方式一
var ptr *int // 声明指针类型
ptr = &a // 初始化指针类型值为变量 a
//方式二
ptr := &a
//方式三
ptr := new(int)
*ptr = 100
fmt.Printf("%p\n", ptr)
fmt.Printf("%d\n", *ptr)
//输出结果:
0xc0000a2000
100
上面代码中的 ptr 就是一个指针类型,表示指向存储 int 类型值的指针。ptr 本身是一个内存地址值,当指针被声明后,没有指向任何变量内存地址时,它的零值是 nil,所以需要通过内存地址进行赋值(通过 &a 可以获取变量 a 所在的内存地址),赋值之后,可以通过 *ptr 获取指针指向内存地址存储的变量值(我们通常将这种引用称作「间接引用」)
使用场景
指针在 Go 语言中有两个典型的使用场景:
- 类型指针
- 切片
作为类型指针时,允许对这个指针类型数据指向的内存地址存储值进行修改,传递数据时如果使用指针则无须拷贝数据从而节省内存空间,此外和 C 语言中的指针不同,Go 语言中的类型指针不能进行偏移和运算,因此更为安全。
切片类型我们前面已经介绍过,由指向数组起始元素的指针、元素数量和容量组成,所以切片与数组不同,是引用类型,而非值类型。
unsafe.Pointer
我们前面介绍的指针都是被声明为指定类型的,而 unsafe.Pointer 是特别定义的一种指针类型,它可以包含任意类型变量的地址(类似 C 语言中的 void 类型指针)。Go 官方文档对这个类型有如下四个描述:
- 任何类型的指针都可以被转化为
unsafe.Pointer; unsafe.Pointer可以被转化为任何类型的指针;uintptr可以被转化为unsafe.Pointer;unsafe.Pointer可以被转化为uintptr。