Golang 总结
环境搭建
-
Golang 标准库
:这部分内容了解完基本语法之后,可以自行查看文档学习。不进行系统演示,该文档代码部分内容会涉及
-
下载 Golang:
下载地址
如果无法访问,原因是被墙了,请自行寻找办法下载
windows 下建议下载 .msi
安装包,免配环境变量
-
VS Code 安装扩展:Go 或 Go Nightly
安装完成后新建一个hello.go
文件,重新打开编辑器,将会提示安装一些Go工具
如果安装失败,一般也是被墙的原因,可以通过以下命令切换源地址,然后重试
1
2
3
4
5
6
|
# 七牛云
$ go env -w GOPROXY=https://goproxy.cn
# 阿里云
$ go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/
# 七牛云,重定向:如果七牛云没找到,去源地址拉取
$ go env -w GOPROXY=https://goproxy.cn,direct
|
-
在线查看标准库文档
基本执行命令
1
2
3
4
5
6
7
8
|
# go 打包为 exe 文件,默认在同目录下
$ go build <filename>.go
# 运行文件 exe 文件
$ <filename>
# 不打包直接运行 go 文件
$ go run <filename>.go
# 格式化代码,go 建议所有代码都是一种风格
$ gofmt -w <filename>.go
|
Golang 语法
1. 开始
如果你有其他编程语言的基础,可以尝试开始部分的内容
如果没有,可以选择跳过开始部分的内容,在阅读完
基本数据类型
部分的内容后,回到这里
1.1 变量
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
|
package main // 入口包
import "fmt"
/* 全局变量不能省略 var */
var global string = "global"
func main() { // 入口函数
/* 一对 {} 内部的为局部变量 */
// 1、声明一个变量,默认值为 0
var a int
fmt.Printf("a = %d, type = %T\n", a, a) // a = 0, type = int
// 2、声明并初始化
var (
str1 string = "str1"
str2 string = "str2"
)
fmt.Printf("str1 = %s, type = %T\n", str1, str1) // str1 = str1, type = string
fmt.Printf("str2 = %s, type = %T\n", str2, str2) // str2 = str2, type = string
// 3、声明并初始化,自动推导类型
var b, c = 100, 3.14
fmt.Printf("b = %d, type = %T\n", b, b) // b = 100, type = int
fmt.Printf("b = %f, type = %T\n", c, c) // b = 3.140000, type = float64
// 4、省略 var(无法定义全局变量,只在方法体中使用)
char := 'A'
fmt.Printf("char = %c, charCode = %d, type = %T\n", char, char, char) // char = A, charCode = 65, type = int32
}
|
1.2 常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import "fmt"
const (
MALE = 1
FEMALE = 2
)
const (
a, b = iota + 1, iota + 3 // iota = 0, a = 1, b = 3
c, d // iota = 1, c = 2, d = 4
e, f // iota = 2, e = 3, f = 5
g, h = iota * 10, iota * 20 // iota = 3, g = 30, h = 60
i, j // iota = 4, i = 40, j = 80
)
func main() {
fmt.Printf("MALE = %d, FEMALE = %d\n", MALE, FEMALE) // MALE = 1, FEMALE = 2
}
|
1.3 命名约定
- 严格区分大小写
package main
包为程序入口文件,有且必须唯一。func main
函数为每个文件的入口函数。
- 包名和文件名不要求一致,但尽量保持一致,且不与标准库冲突。程序自定义的包通过路径导入
- 不允许未使用的变量,存在则编译不通过
- 变量、常量、函数名建议 驼峰命名
- 约定大写字母开头的可以被包外部使用,小写字母开头的只能包内部使用
2. 数据类型
2.1 基本数据类型
这是字符串格式化输出对照表,如果无法理解,可以略过这里,先查看后面的内容
格 式 |
描 述 |
%v |
按值的本来值输出 |
%+v |
在 %v 基础上,对结构体字段名和值进行展开 |
%#v |
输出 Go 语言语法格式的值 |
%T |
输出 Go 语言语法格式的类型和值 |
%% |
输出 % 本体 |
%b |
整型以二进制方式显示 |
%o |
整型以八进制方式显示 |
%d |
整型以十进制方式显示 |
%x |
整型以十六进制方式显示 |
%X |
整型以十六进制、字母大写方式显示 |
%U |
Unicode 字符 |
%f |
浮点数 |
%p |
指针,十六进制方式显示 |
%q |
整数:单引号包裹的字符;字符串:以双引号的形式包裹 |
\n |
输出换行 |
\t |
输出制表符 |
\r |
回到字符串开头,用后面的内容逐个替换 |
\b |
回退一个字符 |
\\ |
输出一个 \ |
\" |
输出一个 " |
\' |
输出一个 ' |
2.1.1 数值型
有符号整型
类型 |
有无符号 |
占用空间 |
表数范围 |
int8 |
√ |
1 byte = 8bit |
(-2^7) ~ (2^7-1) |
int16 |
√ |
2 byte |
(-2^15) ~ (2^15-1) |
int32 |
√ |
3 byte |
(-2^31) ~ (2^31-1) |
int64 |
√ |
4 byte |
(-2^63) ~ (2^63-1) |
无符号整型
类型 |
有无符号 |
占用空间 |
表数范围 |
uint8 |
× |
1 byte = 8bit |
(0) ~ (2^8-1) |
uint16 |
× |
2 byte |
(0) ~ (2^16-1) |
uint32 |
× |
3 byte |
(0) ~ (2^32-1) |
uint64 |
× |
4 byte |
(0) ~ (2^64-1) |
其他整型:开发常用的类型
类型 |
有无符号 |
占用空间 |
int |
√ |
32位系统(同 int32):4 byte; 64位系统(同 int64):8 byte |
uint |
× |
32位系统(同 uint32):4 byte; 64位系统(同 uint64):8 byte |
uintptr |
× |
同 uint,用于存储指针的整型,可与指针类型互相转换 |
byte |
× |
1 byte(同 uint8) |
rune |
√ |
4 byte(同 int32) |
浮点类型
类型 |
有无符号 |
占用空间 |
float32 |
× |
4 byte |
float64(默认) |
× |
4 byte |
如何选择使用哪种类型?
答:Golang 中保小不保大原则,即保证程序运行的情况下,尽量使用占用空间小的数据类型。浮点默认选择float64 是因为浮点类型容易丢失精度
2.1.2 字符类型
Golang 中字符采用 Unicode
字符集,编码格式为 utf-8
,字面量表示使用 'A'
底层是用无符号整型存储,因此没有类型Java中 char
关键字 ,直接使用整型进行赋值,查看以下示例
1
2
3
4
5
6
7
|
import "fmt"
func main() {
var c int = 'A' // 定义一个整型变量并初始化为 'A',必须为单引号,双引号表示的是字符串字面量
fmt.Printf("%d", c) // 控制台打印:65
fmt.Printf("%c", c) // 控制台打印:65
}
|
2.1.3 布尔类型
很简单,没有什么特别的内容,字面量表示使用 true
false
1
2
3
4
5
6
7
|
import "fmt"
func main() {
var flag bool // 默认为 false
fmt.Printf("%T\n", flag) // bool
fmt.Printf("%v", flag) // false
}
|
2.1.4 字符串类型
如果理解了字符类型的存储方式,很容易可以知道,字符串底层是用整型数组存储的,字面量表示使用 "abc"
或模板字符串
1
2
3
4
5
6
7
8
9
10
11
|
import "fmt"
func main() {
var str string = "ABC" + "DEF" // + 拼接
str2 := `fmt.Printf("str2")` // 省略 var;类型推断;模板字符串原样输出
fmt.Println(str)
fmt.Println(str2)
// 我们知道,字符串底层还是数组存储的,假如我们有以下操作
str[0] = 'C' // error,原因是定义好的字符串,值不可再被修改
fmt.Printf("%v", str[0]) // 65
}
|
2.1.5 基本数据类型的默认值与类型转换
默认值满足:
- 数值或字符类型:0
- 布尔类型:false
- 字符串类型:""
基本数据类型转换有以下特点
-
没有隐式转换,必须强制转换
-
数值型转换:如T(v),T表示目标类型,v表示被转换的值
-
大类型转小类型不会出错,但可能会数据溢出导致丢失精度
-
不同类型的数据无法进行运算,编译不通过
1
2
|
var n1 int8 = 100
var n2 int8 = n1 + 128 // error,推断 = 右侧为 int8 类型运算,128溢出编译失败
|
-
非字符串转字符串:使用 fmt.Sprintf()
或 strconv.format*()
-
字符串转非字符串:使用 strconv.Parse*()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 导入多个包,推荐这种方式
import (
"fmt"
"strconv"
)
func main() {
var str string = "100"
num, err := strconv.ParseInt(str, 10, 64)
fmt.Println(num) // 100
fmt.Println(err) // <nil> 表示成功
str = "golang"
num, err := strconv.ParseInt(str, 10, 64)
fmt.Println(num) // 0
fmt.Println(err) // error 表示失败,返回目标类型的默认值
}
|
现在,如果你跳过开始部分的内容,可以选择
回到开始部分
2.2 派生数据类型
在开始这一节之前,先了解几个会使用到的全局内建函数。如果不太理解,后面遇到再回来对照
-
func panic(v interface{})
:停止当前Go程的正常执行(恐慌),v 为引起恐慌的变量
-
func new(Type) *Type
:为一个数据类型分配内存,并返回该类型的指针
-
func len(v Type) int
:返回 v 的长度(已使用的内存空间),这取决于具体类型
- 数组:v中元素的数量
- 数组指针:*v中元素的数量(v为nil时panic)
- 切片、映射:v中元素的数量;若v为nil,len(v)即为零
- 字符串:v中字节的数量
- 信道:信道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零
-
func cap(v Type) int
:返回 v 的容量(分配的内存空间),这取决于具体类型
- 数组:v中元素的数量,与 len(v) 相同
- 数组指针:*v中元素的数量,与len(v) 相同
- 切片:切片的容量(底层数组的长度);若 v为nil,cap(v) 即为零
- 信道:按照元素的单元,相应信道缓存的容量;若v为nil,cap(v)即为零
-
func make(Type, size IntegerType) Type
:分配并初始化一个类型为切片、映射、或通道的对象
- 切片:size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,容量为10的切片。
- 映射:初始分配的创建取决于size,但产生的映射长度为0。size可以省略,这种情况下就会分配一个小的起始大小。
- 信道:信道的缓存根据指定的缓存容量初始化。若 size为零或被省略,该信道即为无缓存的。
-
func append(slice []Type, elems ...Type) []Type
:将元素追加到切片的末尾,包括字符串,容量不足扩容2倍
-
func copy(dst, src []Type) int
:将元素从来源切片复制到目标切片中,包括字符串,复制长度为,长度较小的那个参数
-
func delete(m map[Type]Type1, key Type)
:按照指定的键将元素从映射中删除,若m为nil或无此元素,delete不进行操作
-
func close(c chan<- Type)
:关闭信道,该信道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该信道
以上内容节选自
Golang 标准库文档
,builtin
模块下,更多内容可以自行查看
2.2.1 指针
在 Golang 中,指针只有简单的寻址操作,没有复杂的计算操作
&
通过变量找地址,返回结果是一个指针
*
通过指针找变量,返回结果是变量的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import "fmt"
func main() {
var str string = "100"
var ptr *string = &str
fmt.Println(ptr) // str 的内存地址:0x********
fmt.Println(*ptr) // str 的值:100
fmt.Println(&ptr) // ptr 的内存地址:0x********
/* 指针使用注意事项 */
// 1、可以通过指针修改变量的值,但需要类型匹配
*ptr = "200" // success
*ptr = 200 // error
// 2、指针类型的赋值需要指针类型匹配
var ptr2 *string = ptr // success
var ntr *int = ptr // error
// 3、所有的基本类型都有对应的指针类型
}
|
2.2.2 数组(array)
在 Golang 中,数组特指定长数组:即占用空间固定,无法被更改
1
2
3
4
5
6
|
// 定义一个数组,不初始化
var arr [3]int // arr = [0 0 0]
// 定义一个数组,并初始化
var arr2 [3]int = [3]int{1,2,3}
// 省略var
arr3 := [3]int{4,5,6}
|
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
|
import "fmt"
func main() {
// 定义一个定长数组
var arr [3]int
// 1、查看长度、容量、内容
// 结果:len = 3, cap = 3, arr = [0 0 0]
fmt.Printf("len = %d, cap = %d, arr = %v\n", len(arr), cap(arr), arr)
// 往 arr 整型数组末尾追加一个元素 1
append(arr, 1) // error,定义定长数组无法被扩容
// 2、变量参数传递:值拷贝
// 结果:没有变化
todoArr(arr)
fmt.Printf("len = %d, cap = %d, arr = %v\n", len(arr), cap(arr), arr)
// 3、指针参数传递
// 结果:数组第一个值变为 100
todoArr2(&arr)
fmt.Printf("len = %d, cap = %d, arr = %v\n", len(arr), cap(arr), arr)
}
// 定长数组参数的变量传递必须长度也是一致的,且是值拷贝,不会影响原数组
func todoArr(arr [3]int) {
arr[0] = 100 // 修改第一个长度
// 遍历,使用range
for index, value := range arr {
fmt.Printf("[%d], %v", index, value)
}
}
// 定长数组参数的指针传递也必须长度也是一致的,但会影响原数组
func todoArr2(arrptr *[3]int) {
(*arrptr)[0] = 100
}
|
2.2.3 切片(slice)
在 Golang 中,切片指动态数组,占用内存空间也固定,但是可扩容
1
2
3
4
5
6
|
// 定义一个切片,不初始化
var slice []int // slice = [] = nil
// 定义一个切片,并初始化
var slice2 []int = []int{1,2,3}
// 省略var,分配空间
slice3 := make([]int, 3, 5) //长度为3,容量为5
|
- 切片的操作:所有数组的特性切片都有;所有切片的不影响长度的操作数组都有
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
45
46
47
48
49
|
import "fmt"
func main() {
// 定义一个切片,不初始化
var slice []int
// 1、如果不初始化使用前需要先分配空间
// 结果:len = 3, cap = 5, slice = [0 0 0]
slice = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(slice), cap(slice), slice)
// 2、参数传递
// 结果:len = 3, cap = 3, slice = [100 0 0]
todoArr(slice)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(slice), cap(slice), slice)
// 3、追加元素与扩容,旧的 slice 会被垃圾回收(GC)
// 结果:len = 6, cap = 10, slice = [100 0 0 1 2 3]
slice = append(slice, 1, 2, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(slice), cap(slice), slice)
// 4、切片的拷贝
// 结果:len = 4, cap = 4, slice2 = [100 0 0 1]
slice2 := make([]int, 4)
copy(slice2, slice)
fmt.Printf("len = %d, cap = %d, slice2 = %v\n", len(slice2), cap(slice2), slice2)
// 5、切片的读取
// 结果:len = 2, cap = 10, slice3 = [100 0]
slice3 := slice[0:2]
fmt.Printf("len = %d, cap = %d, slice3 = %v\n", len(slice3), cap(slice3), slice3)
// 6、特例:字符串的切片操作
str := "world!"
slice4 := append([]byte("hello "), 'A')
slice5 := make([]byte, 6)
copy(slice5, []byte(str))
fmt.Println(slice4) // [104 101 108 108 111 32 65]
fmt.Println(slice5) // [119 111 114 108 100 33]
}
// 切片参数传递是引用传递,会影响原切片
func todoArr(slice []int) {
slice[0] = 100 // 修改第一个长度
// 遍历,使用range
for index, value := range slice {
fmt.Printf("[%d], %v\n", index, value)
}
}
|
2.2.4 映射(map)
Golang 中 map 需要注意的点:
- map 是无序的,无法进行有序遍历
- map 是线程不安全的,在多线程编程中,读写操作需要加锁
1
2
3
4
5
6
7
8
9
|
// 定义一个映射,不初始化
var record map[string]string // record = map[] = nil
// 定义一个映射,并初始化
var record2 map[string]string = map[string]string{
"中国": "北京",
"美国": "纽约",
}
// 省略 var,分配空间
record3 := make(map[string]string, 2) // 第二个参数可省略
|
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
|
import "fmt"
func main() {
// 定义一个映射,分配空间
record := make(map[string]string)
// 结果:len = 0, cap = 0, record = []
fmt.Printf("len = %d, record = %v\n", len(record), record)
// 新增
record["中国"] = "北京"
record["美国"] = "纽约"
// 修改
record["美国"] = "华盛顿"
// 删除
delete(record, "美国")
// 查看
fmt.Printf("%s\n", record["中国"]) // 北京
// 参数传递:引用传递
todoMap(record)
// 遍历
for key, value := range record {
fmt.Printf("[%s]: %s", key, value) // [中国]: 深圳
}
func todoMap(row map[string]string) {
row["中国"] = "深圳"
}
|
2.2.5 信道(channel)
该类型用于多线程编程,请先查看
多线程章节
、
go 程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import (
"fmt"
)
func subTask(channel chan int) {
defer fmt.Println("sub task end")
fmt.Println("sub task running")
channel <- 66 // 信道写入。当容量不够时,将会阻塞等待
}
func main() {
c := make(chan int) // 无缓存的信道(容量为 1)
go subTask(c)
// <- c //只取不读(丢弃)
num := <-c // 信道读取。当信道内无值时,将会阻塞等待
fmt.Println("num = ", num)
fmt.Println("main task end")
}
|
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
45
|
import (
"fmt"
"time"
)
func subTask(channel chan int) {
defer fmt.Println("sub task end")
// 循环写入 4次
for i := 0; i < 4; i++ {
channel <- 'A' + i
fmt.Printf("sub task running, send = %c\n", 'A'+i)
// 容量满了,关闭信道
if len(channel) == cap(channel) {
close(channel)
break
}
}
}
func main() {
c := make(chan int, 3) // 有缓存的信道(容量为 3)
go subTask(c)
time.Sleep(2 * time.Second) // 睡眠 2s,让子线程写入堵塞
// 循环读取
for i := 0; i < 5; i++ { // 多读取几次
val, ok := <-c
fmt.Printf("main task running, get = %c, ok = %t\n", val, ok)
}
fmt.Println("main task end")
}
// 结果
// sub task running, send = A
// sub task running, send = B
// sub task running, send = C
// sub task end
// main task running, get = A, ok = true
// main task running, get = B, ok = true
// main task running, get = C, ok = true
// main task running, get = , ok = false 信道空
// main task running, get = , ok = false 信道空
// main task end
|
在上面的例子中,如果没有关闭操作,两种情况将会造成阻塞死锁
- 信道为空,子线程不再写入数据:主线程将死锁在信道读取
- 信道为满,主线程不再读取数据:子线程将死锁在信道写入
因此,我们常常需要手动关闭信道的操作,让程序继续正常运行
在上面的例子中,通过 普通 for 循环 多循环两次,得到的结果是空,其实是没必要的
我们可以使用 增强 for 循环 去遍历。
- 信道为空且关闭的时候,自动结束循环
- 信道为空未关闭的时候,阻塞等待
1
2
3
4
|
// 将上面循环读取的代码改为下面的内容
for val := range c {
fmt.Printf("main task running, get = %c\n", val)
}
|
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
|
import (
"fmt"
)
// 斐波那契数列生成函数,长度为 channel 的长度
func fibnacii(channel, quit chan int) {
x, y := 1, 1
for {
select {
case <-quit: // 如果 quit 可读,不再写入
return
case channel <- x: // 如果 channel 可写,写入数据
temp := x
x = y // 将写入的数改为保存的
y += temp // 保存下一个数
}
}
}
// 读取数列的函数
func readFib(channel, quit chan int) {
defer fmt.Println("\nsub task end")
fmt.Println("reading")
for i := 0; i < cap(channel); i++ {
val := <-channel
fmt.Printf("%d ", val)
}
quit <- 1
}
func main() {
c := make(chan int, 10) // 有缓存的信道(容量为 10)
q := make(chan int)
go readFib(c, q)
fibnacii(c, q)
}
// 结果
// reading
// 1 1 2 3 5 8 13 21 34 55
// sub task end
|
2.2.6 结构体(struct)
结构体是将多种不同的类型组合在一起,形成新的类型。结构体是
面向对象
的基础
- 结构体的定义与基本操作,更多内容在面向对象章节展示
- 关于属性标签的实际应用场景之一,前往
JSON转换
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
|
import "fmt"
// 定义一个学生结构体
type Student struct {
name string `info:"学生姓名"` // 属性标签
age uint8
}
func changeStu(s Student) {
s.age = 20
}
func changeStu2(s *Student) {
s.age = 25
}
func main() {
// 初始化
stu := Student{name: "Saly", age: 18}
fmt.Printf("%s is %d years old\n", stu.name, stu.age) // Saly is 18 years old
// 更改值
stu.age = 19
// 变量入参:值拷贝
changeStu(stu)
fmt.Println(stu) // {Saly 19}
// 指针入参
changeStu2(&stu)
fmt.Println(stu) // {Saly 25}
}
|
2.2.7 接口(interface)
万能类型 type interface{}
,Golang 中所有的类型都实现了这个接口,类似于Java 中的 Object,TypeScript 中的 any
完整使用移步
面向对象多态
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
|
import "fmt"
type Student struct {
name string
age uint8
}
func show(param interface{}) {
// 类型断言
value, ok := param.(Student)
switch ok {
case true:
fmt.Printf("is Student, value = %v\n", value)
case false:
fmt.Printf("is not Student, value = %v\n", value)
}
}
func main() {
// 初始化
stu := Student{name: "Saly", age: 18}
show(stu) // is Student, value = {Saly 18}
show("name") // is not Student, value = { 0}
show(100) // is not Student, value = { 0}
show(true) // is not Student, value = { 0}
}
|
2.2.8 自定义类型
Golang 中为简化数据定义,支持自定义数据
1
2
3
4
5
6
7
8
9
10
11
|
import "fmt"
type myInt int
func main() {
var num myInt = 10
var num2 int = 20
// int 和 myInt 虽然最终都是 int 类型
// 但是 Golang 认为不一样,不能直接赋值和运算,需要强制转换
num = myInt(num2)
fmt.Println(num) // 20
}
|
如果查看源码可以发现,基本数据类型中的 rune
是 int32
的别名,byte
是 unit8
的别名
2.2.9
函数
3. 运算符
如果有其他语言的基础,以下列举了 Java
、JavaScript
中的主要差异
细节方面可以自行了解,然后这一章节可以直接跳过
-
Java:
- Golang 中没有三目运算符(
? :
)、取反运算符(~
)
- Golang 中多了两个
指针运算符
&<name>
*<name>
,一个
信道操作符
<-
-
JavaScript:
- 在与 Java 比较的基础上,Golang 没有无符号左右移(
<<<
、>>>
)
3.1 算术运算符
下表列出了所有Go语言的算术运算符。假定 A = 10,B = 20
运算符 |
描述 |
实例 |
+ |
相加 |
A + B 输出结果 30 |
- |
相减 |
A - B 输出结果 -10 |
* |
相乘 |
A * B 输出结果 200 |
/ |
相除 |
B / A 输出结果 2 |
% |
求余 |
B % A 输出结果 0 |
++ |
自增 |
A++ 输出结果 11 |
– |
自减 |
A– 输出结果 9 |
3.2 关系运算符
下表列出了所有Go语言的关系运算符。假定 A = 10,B = 20
运算符 |
描述 |
实例 |
== |
检查两个值是否相等,如果相等返回 true 否则返回 false。 |
(A == B) 为 false |
!= |
检查两个值是否不相等,如果不相等返回 true 否则返回 false。 |
(A != B) 为 true |
> |
检查左边值是否大于右边值,如果是返回 true 否则返回 false。 |
(A > B) 为 false |
< |
检查左边值是否小于右边值,如果是返回 true 否则返回 false。 |
(A < B) 为 true |
>= |
检查左边值是否大于等于右边值,如果是返回 true 否则返回 false。 |
(A >= B) 为 false |
<= |
检查左边值是否小于等于右边值,如果是返回 true 否则返回 false。 |
(A <= B) 为 true |
3.3 逻辑运算符
下表列出了所有Go语言的逻辑运算符。假定 A = true,B = false
运算符 |
描述 |
实例 |
&& |
逻辑 AND 运算符。 如果两边的操作数都是 true,则条件 true,否则为 false。 |
(A && B) 为 false |
|| |
逻辑 OR 运算符。 如果两边的操作数有一个 true,则条件 true,否则为 false。 |
(A || B) 为 true |
! |
逻辑 NOT 运算符。 如果条件为 true,则逻辑 NOT 条件 false,否则为 true。 |
!(A && B) 为 true |
3.4 位运算符
Go 语言支持的位运算符如下表所示。假定 A = 60,B =13
运算符 |
描述 |
实例 |
& |
按位与运算符"&“是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 |
(A & B) 结果为 12, 二进制为 0000 1100 |
| |
按位或运算符”|“是双目运算符。 其功能是参与运算的两数各对应的二进位相或 |
(A | B) 结果为 61, 二进制为 0011 1101 |
^ |
按位异或运算符”^“是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 |
(A ^ B) 结果为 49, 二进制为 0011 0001 |
« |
左移运算符”«“是双目运算符。左移n位就是乘以2的n次方。 其功能把”«“左边的运算数的各二进位全部左移若干位,由”«“右边的数指定移动的位数,高位丢弃,低位补0。 |
A « 2 结果为 240 ,二进制为 1111 0000 |
» |
右移运算符”»“是双目运算符。右移n位就是除以2的n次方。 其功能是把”»“左边的运算数的各二进位全部右移若干位,"»“右边的数指定移动的位数。 |
A » 2 结果为 15 ,二进制为 0000 1111 |
3.5 赋值运算符
下表列出了所有Go语言的赋值运算符
运算符 |
描述 |
实例 |
= |
简单的赋值运算符,将一个表达式的值赋给一个左值 |
C = A + B 将 A + B 表达式结果赋值给 C |
+= |
相加后再赋值 |
C += A 等于 C = C + A |
-= |
相减后再赋值 |
C -= A 等于 C = C - A |
*= |
相乘后再赋值 |
C *= A 等于 C = C * A |
/= |
相除后再赋值 |
C /= A 等于 C = C / A |
%= |
求余后再赋值 |
C %= A 等于 C = C % A |
«= |
左移后赋值 |
C «= 2 等于 C = C « 2 |
»= |
右移后赋值 |
C »= 2 等于 C = C » 2 |
&= |
按位与后赋值 |
C &= 2 等于 C = C & 2 |
^= |
按位异或后赋值 |
C ^= 2 等于 C = C ^ 2 |
|= |
按位或后赋值 |
C |= 2 等于 C = C | 2 |
3.6 指针运算符
下表列出了Go语言的指针运算符
运算符 |
描述 |
实例 |
& |
返回变量存储地址 |
&a; 将给出变量的实际地址。 |
* |
指针变量。 |
*a; 是一个指针变量 |
3.7 信道操作符
运算符 |
描述 |
<- |
右边为 channel 类型,信道读取。右边为非 channel 类型,信道写入 |
3.8 运算符优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低
优先级 |
运算符 |
5 |
* / % « » & &^ |
4 |
+ - | ^ |
3 |
== != < <= > >= <- |
2 |
&& |
1 |
|| |
3.9 获取用户输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import "fmt"
func main() {
var name string
var age uint8
var isGraduate bool
/* 单个输入 */
fmt.Println("请输入学生信息:")
fmt.Print("姓名:")
fmt.Scanln(&name)
fmt.Print("年龄:")
fmt.Scanln(&age)
fmt.Print("是否毕业:")
fmt.Scanln(&isGraduate)
/* 批量输入 */
fmt.Println("请输入学生信息(空格分隔):")
fmt.Scanf("%s %d %t", &name, &age, &isGraduate)
fmt.Printf("%s %d %t", name, age, isGraduate)
}
|
如果用户类型不匹配,或者传递的参数不是一个地址,虽然不会报错,但是输入也无法被获取
4 . 流程控制语句
4.1 条件表达式
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
|
import "fmt"
func main() {
var score uint8 = 100
/* if else */
if score >= 90 { // 大括号必须写
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
/* switch */
// switch 后面可以是一个表达式;也可为空(相当于 if else);还可以定义变量,此时必须 ; 结尾
switch score / 10 {
default: // 位置任意,所有分支不满足走这里
fmt.Println("C")
case 9, 10: // 可以带多个值
fmt.Println("A")
fallthrough // 穿透这个分支,下一个分支语句也执行
case 8:
fmt.Println("B")
break // 可省略
}
}
|
4.2 for 循环 & goto
Golang 中没有 while
循环
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
|
func main() {
var text string = "Hello, Golang"
for i := 0; i < len(text); i++ {
if text[i] == 'G' {
goto label // 跳转到 label; 位置
}
if i == 5 {
continue // 跳过当前循环,逗号字符
}
if i == 6 {
break // 跳出循环,空格字符
}
label;
if text[i] == 'G' {
return // 跳出循环并返回函数结果
}
fmt.Printf("[%d] %c\n", i, text[i])
}
//也可以这样写
j := 0
for i < len(text) {
fmt.Printf("[%d] %c\n", j, text[j])
j++
}
}
|
场景:如果字符串中含中文,用普通 for 循环是会乱码的
原因:普通 for 循环每次遍历 1 个字节,而中文占 3~4 字节
办法:range for 循环。此循环还可用于遍历
派生数据类型
中的——数组、切片、map、通道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() {
var text string = "你好, Go语言"
for index, value := range text {
fmt.Printf("[%d] %c\n", index, value)
}
// 结果
/*
* [0] 你
* [3] 好
* [6] ,
* [7]
* [8] G
* [9] o
* [10] 语
* [13] 言
*/
}
|
5. 函数
注意事项:
- 函数也是一种数据类型,可以赋值给一个变量,也可作为另一个函数的形参
- 函数不支持重载
- 基本数据类型和数组:形参是值传递,对形参的值更改不会影响函数外部变量的值
- 如果形参传递的是指针,那么会影响函数外部变量的值
5.1 定义使用一个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import "fmt"
func main() {
a := 20
b := 30
fmt.Printf("a = %d, b = %d\n", a, b) // a = 20, b = 30
swap(&a, &b)
fmt.Printf("a = %d, b = %d\n", a, b) // a = 30, b = 20
}
// 交换两数:改变原数
func swap(a *int, b *int) {
*a = *a + *b
*b = *a - *b
*a = *a - *b
}
|
5.2 函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import "fmt"
func main() {
a := 20
b := 30
fmt.Printf("a = %d, b = %d\n", a, b) // a = 20, b = 30
c, d := swap(a, b)
fmt.Printf("a = %d, b = %d\n", a, b) // a = 20, b = 30
fmt.Printf("c = %d, d = %d\n", c, d) // c = 30, d = 20
// 不需要的返回值使用 _ 替代,不可省略,否则无法编译通过
e, _ := swap(100, 200)
fmt.Printf("e = %d", e) // e = 200
}
// 交换两数:不会改变原数
func swap(a int, b int) (int, int) { // 定义两个返回值,当只有一个的时候可以省略 ()
a = a + b
b = a - b
a = a - b
return a, b // 返回的数据类型需对应:开头定义的类型
}
|
5.3 可变参数
1
2
3
4
5
6
7
8
9
10
11
12
|
import "fmt"
func main() {
f := fn // 函数也是一种数据类型,可以赋值给一个变量。等价于:var f func(args ...int) = fn
f('A', 'B', 'C', 'D', 'E')
}
func fn(args ...int) {
for index, value := range args {
fmt.Printf("[%d] %c\n", index, value)
}
}
|
5.4 函数参数与返回值命名
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
|
import "fmt"
// 定义一个函数类型:参数 —— int, int;无返回值
type callback func(int, int)
func main() {
// 定义一个两数相除的函数
divide := func(num1 int, num2 int) {
fmt.Printf("%d\n", num1/num2) // 96/12 = 8
}
a, b := 55, 43
sum, sub := sumSub(a, b, divide) // 返回之前执行了 divide 函数
fmt.Printf("sum = %d, sub = %d", sum, sub) // sum = 98, sub = 12
}
// 定义一个函数类型
type myFunc func(int, int) (int, int)
// 求两个数的 和、差
func sumSub(a int, b int, back callback) (sum int, sub int) { // 这里命名了两个返回值:sum, sub
sum = a + b
sub = a - b
back(sum, sub)
return // 直接将命名的两个返回值返回
}
|
5.5 匿名函数
1
2
3
4
5
6
7
8
9
|
import "fmt"
func main() {
ret := func(a int, b int) (sum int) {
sum = a + b
return
}(10, 20)
fmt.Println(ret) // 30
}
|
5.6 闭包
先查看下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import "fmt"
type fn func(int) int
func getSum() fn {
var sum int = 0
return func(val int) int {
sum += val
return sum
}
}
func main() {
f := getSum()
fmt.Println(f(1)) // 1
fmt.Println(f(2)) // 3
fmt.Println(f(3)) // 6
fmt.Println(f(4)) // 10
}
|
期望的结果应该是:1,2,3,4。实际结果却是:1,3,6,10
原因在于 func getSum()
内部的 sum
与返回的匿名函数形成了闭包。在内存上 sum
不会被释放
闭包产生的原因:
- 存在函数嵌套
- 函数内部的函数,引用了外部函数定义的变量
- 内部函数被调用
**闭包的本质:**本质是一个函数,只是引用了外部函数定义的变量
闭包的应用场景:
- 状态持久化的执行场景:如统计
- 时间相关的执行场景:如动画、节流防抖
- 函数内部状态的获取:如回调
**闭包的缺陷:**内存占用
5.7 defer 手动压栈
直接查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import "fmt"
func getSum(a int, b int) int {
defer fmt.Printf("a = %d\n", a) // 压入调用栈,同时保存 a 的值
defer fmt.Printf("b = %d\n", b) // 压入调用栈,同时保存 b 的值
a += 10
b += 20
return a + b // 执行完之后,以此取出 defer 压入栈的内容执行
}
func main() {
fmt.Printf("sum = %d", getSum(5, 15))
/**
* 返回结果如下:
* b = 15
* a = 5
* sum = 50
*/
}
|
5.8 go程
通过 go
关键字,将一个函数的调用,放入子线程执行。用于
多线程编程
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
|
import (
"fmt"
"runtime"
"time"
)
func subTask() {
defer fmt.Println("子线程关闭")
for i := 0; i < 5; i++ {
// 提前退出
if i == 2 {
runtime.Goexit()
}
fmt.Printf("sub task: %d\n", i)
time.Sleep(1 * time.Second) // 睡眠 1s
}
}
func main() {
go subTask() // 创建子线程调用
for i := 0; i < 5; i++ {
fmt.Printf("main task: %d\n", i)
time.Sleep(1 * time.Second) // 睡眠 1s
}
fmt.Println("主线程执行结束") // 如果主线程提前结束,所有子线程将会全部退出
}
|
通过 go
创建的子线程是无法接受函数返回值的,此时需要使用 channel
进行通信
6. 包
6.1 包的使用
-
每个 .go
文件,开头必须使用 package
关键字声明包名
-
同个文件夹下的所有 .go
文件属于同一个包,因此包名需保持一致
-
package main
为入口包,一个程序必须含有。func main()
为入口函数,每个文件必须含有
-
包名可以随意定义,但是建议和文件名一致,且不要和标准库冲突
-
包导入可以使用路径导入,可以是绝对路径或相对路径。相对路径从 $GOPATH/src
开始,其中 GOPATH
为系统环境变量(已废弃)。绝对路径和默认相对路径对开发并不友好,可移步
模块化
-
导入多个包建议以下写法
1
2
3
4
|
import (
"fmt"
"strconv"
)
|
-
包导入可以定义别名。定义后,在当前文件,被导入的包名不能再使用
1
2
3
4
5
6
7
8
|
import (
"fmt"
pstr "strconv"
)
func main() {
pstr.FormatInt("100") // 而不再是 strconv.FormatInt()
}
|
-
调用包中定义的内容需要 包名.
开始
1
2
3
4
5
|
import "fmt"
func main() {
fmt.Println("hello")
}
|
6.2 包的执行顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import "fmt" // 1、被导入包的全局变量执行。2、被导入包的 func init() 执行
// 3、本包的全局变量被执行,函数定义、全局变量定义和初始化会提升
func getNum() int {
return 10
}
var num int = getNum()
// 4、本包的 init 函数执行。函数执行时,局部变量定义和初始化将会提升
func init() {
fmt.Println(num) // 10
num++
}
// 本包的 main 函数执行
func main() {
fmt.Println(num) // 11
}
|
7. 面向对象
7.1 封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import "fmt"
/* 所有变量、方法、结构体:大写表示向外暴露,小写表示私有 */
// 定义一个类
type Animal struct {
kind string
color string
}
// 定义 Animal 的方法
func (this *Animal) show() { // 这里如果不是指针,将会是值拷贝
fmt.Printf("%s's color is %s\n", this.kind, this.color)
}
func main() {
animal := Animal{kind: "cat", color: "white"}
animal.show() // cat's color is white
}
|
7.2 继承
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
|
// 定义猫类,继承 Animal
type Cat struct {
Animal // 直接继承
age uint8 // 新的属性
}
// 重写 show 方法
func (this *Cat) show() {
this.Animal.show() // 调用父类的方法,属性同
fmt.Printf("age is %d\n", this.age)
}
// 新的 eat 方法
func (this *Cat) eat() {
fmt.Printf("%s's is eating", this.kind)
}
func main() {
var cat Cat
cat.kind = "cat"
cat.color = "white"
cat.age = 1
cat.show() // cat's color is white \n age is 1
cat.eat() // cat's is eating
}
|
7.3 多态
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
45
46
47
48
49
50
51
52
|
import "fmt"
// 定义接口,本质是一个指针
type AnimalIF interface {
sleep()
eat()
}
// Cat 类,只需要实现 AnimalIF 的全部方法
type Cat struct {
name string
}
func (this *Cat) sleep() {
fmt.Printf("(Cat)%s is sleeping\n", this.name)
}
func (this *Cat) eat() {
fmt.Printf("(Cat)%s is eating\n", this.name)
}
// Dog 类,只需要实现 AnimalIF 的全部方法
type Dog struct {
name string
}
func (this *Dog) sleep() {
fmt.Printf("(Dog)%s is sleeping\n", this.name)
}
func (this *Dog) eat() {
fmt.Printf("(Dog)%s is eating\n", this.name)
}
// 定义一个方法,执行 Animal 的动作
func dosth(animal AnimalIF) {
animal.sleep()
animal.eat()
}
func main() {
cat := Cat{name: "Tom"}
dog := Dog{name: "Jim"}
// 注意这里传的是指针,因为 interface 本质是一个指针
dosth(&cat)
dosth(&dog)
}
// 结果
// (Cat)Tom is sleeping
// (Cat)Tom is eating
// (Dog)Jim is sleeping
// (Dog)Jim is eating
|
7.4 反射
概念:通过变量获取其对应类型,从而获取整个类型结构,根据需要做一些操作
7.4.1 反射原理
Golang 中反射的实现基于:变量的 pair
结构,和 interface
的 pair
传递
pair 结构
定义一个变量 var num int = 10
,其内部结构包含一个 type=int
和一个 value=10
如果是一个指针类型(interface 除外,它本质就是指针),那么 type=*Type
保存的是指针类型
interface 的 pair 传递
传递一个基本类型
1
2
3
4
5
6
7
8
9
10
11
12
|
import "fmt"
func main() {
var str string
str = "hello" // pair<type: string, value: "hello">
var any interface{}
any = str // any 的 pair 结构仍然是:pair<type: string, value: "hello">
newStr, _ := any.(string) // 所以这里可以类型断言成功
fmt.Println(newStr) // hello
}
|
传递一个复杂类型
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
|
import "fmt"
type ReadBook interface {
read()
}
type WriteBook interface {
write()
}
type Book struct {
name string
}
// 实现 ReadBook 和 WriteBook
func (this *Book) read() {
fmt.Printf("%s is reading\n", this.name)
}
func (this *Book) write() {
fmt.Printf("%s is writing\n", this.name)
}
func main() {
book := &Book{name: "西游记"} // pair<type: Book, value: book地址>
var reader ReadBook
reader = book // pair<type: Book, value: book地址>
reader.read()
var writer WriteBook
writer = reader.(WriteBook) // 因为 pair 传递过程中不变,都是 Book,所以断言成功
writer.write()
}
|
传递一个指针类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 首先当前目录新建一个 `test.txt` 文件
import (
"fmt"
"io"
"os"
)
func main() {
// file: pair<type: *os.File, value: "./test.txt"文件描述符>
file, err := os.OpenFile("./test.txt", os.O_RDWR, 0)
if err != nil {
fmt.Println("open file error")
fmt.Println(err)
return
}
var reader io.Reader = file // pair<type: *os.File, value: "./test.txt"文件描述符>
var writer io.Writer
// *os.File 同时实现了 io.Reader 和 io.Writer 接口
// 所以断言成功,并把 pair 结构传递给 writer
writer = reader.(io.Writer)
writer.Write([]byte("Hello, golang!")) // 成功写入
}
|
这一节解释了 Golang 为什么可以通过变量获取到其对应类型,下一节将展示反射的用法
7.4.2 反射用法
反射的使用通过 reflect
标准包,满足以下条件的 属性或方法,可以被反射获取
- 大写开头的属性
- 大写开头的方法(
go v17+
版本,指针类型传递的方法不再可以通过反射获取)
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
import (
"fmt"
"reflect"
)
type User struct {
Id int `info:"唯一id"` // 属性标签
Name string `info:"名字"`
Age uint8 `info:"年龄" desc:"小于128"`
}
// 实现一个方法
func (this User) Call() {
fmt.Println("User is called..")
fmt.Printf("%v\n", this)
}
func main() {
user := User{1, "Saly", 20}
ref(user)
}
// 通过反射处理传入变量类型结构
func ref(input interface{}) {
// 1、获取类型
fmt.Println("======= type ======")
inputType := reflect.TypeOf(input)
fmt.Println("input type is: ", inputType)
// 2、获取值
fmt.Println("======= value ======")
inputValue := reflect.ValueOf(input)
fmt.Println("input value is: ", inputValue)
// 3、获取字段、值、标签
fmt.Println("======= field:value ======")
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
filedValue := inputValue.Field(i).Interface()
infoTag := field.Tag.Get("info")
descTag := field.Tag.Get("desc")
if len((descTag)) > 0 {
descTag = ", " + descTag
}
fmt.Printf("%s %v = %v [%s%v]\n", field.Name, field.Type, filedValue, infoTag, descTag)
}
// 4、获取方法
fmt.Println("======= method ======")
// 注意:在 golang 17 版本以后,返回的是不传类型指针的方法的数量
mNUm := inputType.NumMethod()
for i := 0; i < mNUm; i++ {
method := inputType.Method(i)
fmt.Printf("%s %v\n", method.Name, method.Type)
}
fmt.Println("======= method by name ======")
// 注意:在 golang 17 版本以后,只能获取不传类型指针的方法
m, ok := inputType.MethodByName("Call")
fmt.Println("ok = ", ok)
fmt.Printf("%s %v\n", m.Name, m.Type)
}
|
7.4.3 JSON转换
- 场景:将一个结构体序列化未 JSON,但是 key 必须小写
- 问题:Golang 中序列化需要通过反射获取字段名和值,但是获取字段需要大写
- 如果小写,序列化属性将会丢失
- 如果大写,不满足需求
- 方案:通过属性标签,指定被序列化的 key
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
|
import (
"encoding/json"
"fmt"
)
type Movie struct {
Id int `json:"id"`
Year uint16 `json:"year"`
Name string `json:"name"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{1, 2000, "喜剧之王", []string{"周星驰", "张柏芝"}}
// 序列化
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error, ", err)
return
}
fmt.Printf("%s\n", jsonStr) // {"id":1,"year":2000,"name":"喜剧之王","actors":["周星驰","张柏芝"]}
// 反序列化
var newMovie Movie
err2 := json.Unmarshal(jsonStr, &newMovie)
if err != nil {
fmt.Println("json unmarshal error, ", err2)
return
}
fmt.Printf("%v\n", newMovie) // {1 2000 喜剧之王 [周星驰 张柏芝]}
}
|
7.5 泛型
go v1.18
新增
**应用场景:**业务逻辑相似,但是类型不一致,例如不同数值类型的运算、比较等
7.5.1 定义泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func main() {
arr1 := []string{"Hello", "World", "Golang"}
arr2 := []int{1, 2, 3, 4, 5}
printSlice(arr1) // success
printSlice(arr2) // success
printSlice2(arr1) // success
printSlice2(arr2) // success
}
// 内置类型 any 可以是任何类型
func printSlice[T any](list []T) {
for _, item := range list {
fmt.Printf("%v\n", item)
}
}
// 内置类型 comparable 可以是任何可比较的类型:包括指针、结构体
func printSlice2[T comparable](list []T) {
for _, item := range list {
fmt.Printf("%v\n", item)
}
}
|
7.5.2 泛型类型&泛型函数
Golang 中有不同类型的切片。我们可以通过泛型类型,定义成统一的类型,在调用时指定切片内的元素类型即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import "fmt"
// 泛型类型
type Slice[T int | float32 | string] []T
// 泛型函数:切片累加
func (list Slice[T]) getSum() (sum T) {
for _, item := range list {
sum += item
}
return
}
func main() {
s1 := Slice[string]{"Hello, ", "World and ", "Golang!"}
s2 := Slice[int]{1, 2, 3, 4, 5}
fmt.Println(s1.getSum()) // Hello, World and Golang!
fmt.Println(s2.getSum()) // 15
}
|
7.5.3 自定义泛型约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import "fmt"
// int 别名
type myInt int
// 泛型约束
// ~ 表示支持该类型的别名
type number interface {
~int | int8 | int16 | float32 | float64
}
// 相加函数
func add[T number](a, b T) T {
return a + b
}
func main() {
var n1, n2 myInt = 10, 20
fmt.Println(add(n1, n2)) // 30
f1, f2 := 3.2, 4.25
fmt.Println(add(f1, f2)) // 7.45
}
|
8. 多线程
8.1 goroutine 模型
- 内核线程(M):负责多线程处理
- 线程控制器(P):连接线程与 goroutine 调度器,从 goroutine 调度器中取出资源(G),交给线程处理
- goroutine 调度器(GM):负责资源(G)的统一管理,根据 P 的需要,提供资源(G)
- 全局队列(GQ):空闲的资源在全局队列中,并且加锁。对全局队列的读取需要解锁(比较耗时)
- goroutine(G):线程最终需要的计算机资源
注:为方便后续内容描述,后面的将使用括号内的简写。如 goroutine 将直接描述为:G
8.2 多线程调度策略
-
复用线程
- 偷取(work stealing)
- 场景:如模型图,假设P3(最右边的P) 本地队列为空,P2 中含 G1 且等待中;M2 请求 G1
- 策略:P3 将从 M2 偷取 G1,然后交给 M2
- 握手(hand off)
- 场景:如模型图,假设P3(最右边的P) 本地队列含 G1、G2;M2 请求 G1,G1阻塞,G2马上要被执行;此时 M3空闲
- 策略:P3 将 G1 交给 M2继续等待,M3 与 P3 握手
-
利用并行:指定多个 P(一般为 CPU 核数的一半)
-
抢占:CPU 处理 G 耗时过长(约 10ms),G 将被新的 CPU 线程抢占
-
全局队列:如果所有的 P 中都没有 M 需要的 G,将会尝试从 GQ 中解锁获取
现在,可以回到
信道章节
查看基本使用
9. 模块化
go modules
需要 [email protected]
以上,淘汰原先的 GOPATH
环境变量模式
9.1 开启模块化
Golang 提供 GO111MODULE
这个环境变量来开启/关闭 go modules
auto
:默认值,项目包含 go.mod
文件则启用
on
:启用(推荐)
off
:禁用
1
2
|
$ go env -w GO111MODULE=on
# 其他环境变量的设置同这个一样
|
9.2 查看环境变量
1
2
3
4
5
6
|
# 查看所有环境变量
$ go env
# 查看部分,限 linux、mac
$ go env |grep <name>
# 查看一个
$go env <name>
|
9.3 其他模块化环境变量
变量名 |
描述 |
值 |
默认值 |
GOSUMDB |
版本校验地址,校验模块版本是否被篡改,篡改则中止 |
地址/off |
sum.golang.org |
GONOPROXY |
设置私有库,不会校验 |
地址,支持通配符* |
|
GOPRIVATE |
设置私有库,不会校验 |
地址,支持通配符* |
|
GONOSUMDB |
设置私有库,不会校验 |
地址,支持通配符* |
|
9.4 初始化 go odules 项目
1
|
$ go mod init github.com/U-Wen/gostudy
|
会生成一个 go.mod
文件,内容如下
1
2
|
module github.com/U-Wen/gostudy
go 1.20
|
1
2
3
|
# 1、go run/build 的时候自动拉取
# 2、手动 down
$ go get <url>
|
如果拉取了非标准库,go.mod
会新增 require 依赖地址,并且会生成 go.sum
文件
该文件作用:统一管理直接或间接依赖的所有模块版本,避免被篡改
- 手动更改依赖的版本
- 直接修改
go.mod
文件
- 执行
go mod edit -replace=<cur>=<tar>
,版本重定向
10. Go 生态
此章节介绍的所有 go 开源库和框架的 star
,记录于 2022-2-21
10.1 Web 框架
- Beego:国内框架,自带 ORM 差点意思,
star = 29.4k
- Gin:国外轻量级框架,
star = 66.6k
- echo:国外轻量级框架,
star = 25k
- Iris:国外框架,
star = 23.6k
10.2 微服务框架
- go zero:集成了各种工程实践的 web 和 rpc 框架,
star = 22.8k
- Go kit:较轻的微服务框架,集成各种 web 框架,最后更新时间
2021-09
,star = 24.6k
- Istio:较全的微服务框架,
star = 32.4k
10.3 容器
-
Kubernetes(k8s)
-
Docker Swarm
10.4 服务发现
-
Consul
10.5 存储引擎
-
etcd
: 分布式 k/v 存储
star = 42.6k
-
tidb
: 分布式 关系数据库,中文文档
star = 33.5k
-
vitess
:分布式 MySQL,中文文档
star = 15.6k
10.6 关系映射
- xorm:
star = 6.6k
- gorm:
star = 31.5k
10.7 日志&配置
-
logrus:star = 22.2k
-
zap:star = 18.2k
-
viper:star = 22.1k
10.8 权限
- casbin:
star = 13.9k
10.9 工作流
- cadence:
star = 6.7k
11. web 实践
11.1 开发工具
假设已安装 go 在电脑上,并已配置好全局变量、开启 go modules
- 如果是学习基本语法等,推荐使用 VS Code,你只需要下载安装后,安装下面三个插件
- Auto Import
- Go 或者 Go Nightly
- Chinese(汉化插件,可选)
- 如果是项目开发,推荐使用 Goland
- 打开 Goland -> Settings -> Go
- 将以下三个配置为与
go env
列出的一致
11.2 gin 实践
首先:手动拉取 gin 库
1
2
3
4
|
$ go get -u github.com/gin-gonic/gin
# 可选,web favicon
go get github.com/thinkerou/favicon
|
然后:新建一个 GO 项目,新建 main.go
文件,添加以下内容
1
2
3
4
5
6
7
8
9
|
package main
import (
"github.com/gin-gonic/gin" // gin 框架包
"github.com/thinkerou/favicon" // favicon 包
"net/http" // 标准包,http 状态等
)
func main() {}
|
现在:启动一个服务,并提供一个前后端分离接口
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
|
// main.go
func init() {
// 如果使用数据库,可以 get 相关依赖,然后在这里简历连接
}
func main() {
/* 创建一个服务 */
gs := gin.Default()
// 使用 favicon
gs.Use(favicon.New("./favicon.ico"))
// 返回 JSON
gs.GET("/hello", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{"msg": "hello gin"})
})
/* 启动服务 */
err := gs.Run(":9800")
if err != nil {
return
}
}
// 访问 localhost:9800/hello
// 得到 JSON 数据
|
或者:启动一个提供 html 页面的服务
- 创建 html 模板
/templates/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<link rel="stylesheet" href="/static/css/global.css" />
</head>
<body>
<button id="btn" about="{{.msg}}">click</button>
<script src="/static/js/global.js"></script>
</body>
</html>
|
- 创建 JS、CSS 等静态文件
/static/js/global.js
/static/css/global.css
1
2
3
4
5
6
7
8
9
|
window.onload = function () {
const btn = document.getElementById("btn")
if (btn) {
btn.addEventListener("click", function (e) {
const about = e.target.getAttribute("about")
alert(about)
})
}
}
|
1
2
3
|
body {
background-color: burlywood;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// main.go
func main() {
/* 创建一个服务 */
gs := gin.Default()
// 使用 favicon
gs.Use(favicon.New("./favicon.ico"))
// 加载模板和静态资源
gs.LoadHTMLGlob("templates/*")
gs.Static("/static", "./static")
// 返回 HTML
gs.GET("/index", func(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{"msg": "返回 HTML 文件"})
})
/* 启动服务 */
err := gs.Run(":9800")
if err != nil {
return
}
}
|
现在,浏览器访问 localhost:9800/index 得到一个页面,并且点击按钮成功提示 返回 HTML 文件
更多内容,
参考官方示例
11.3 gRPC 实践
有关 gRPC 的内容,可以参考文章:
一文快速了解 HTTP、RESTful、RPC、Feign、gRPC 直间的联系
11.3.1 环境准备
首先:需要安装 Protocol Buffers
的执行程序到你的电脑,没有安装程序,直接解压并配置好环境变量
注意:下载的时候一定要找对版本,迭代的版本非常多
然后:准备好后新建一个 go 项目,拉取 grpc
1
|
$ go get google.golang.org/grpc
|
安装 go 的 gRPC 代码生成工具命令到计算机
1
2
3
4
5
6
7
|
# 旧版,不要安装这个
$ go install github.com/protocolbuffers/protobuf-go
# 新版,安装下面两个
$ go install google.golang.org/protobuf/cmd/protoc-gen-go # 生成 go 代码命令
$ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc # 需要先拉取
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc # 生成 go grpc 代码命令
|
生成的命令会在 $GOPATH\bin
目录下,可以查看是否安装成功
继续:创建好客户端和服务端目录
1
2
3
4
5
6
7
8
|
├── client # 客户端
│ ├── proto
│ └── hello.proto # 客户端协议文件
│ └── main.go
├── server # 服务端
│ ├── proto
│ └── hello.proto # 服务端协议文件
│ └── main.go
|
最后:安装好相关插件,支持代码提示,高亮等
11.3.2 proto 代码生成
首先:编写 proto
约束文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 语法格式
syntax = "proto3";
// 生成的 go 代码目录和包名
// ; 前表示生成代码的相对目录,; 后表示生成代码的包名
option go_package = "../apis/hello;hello";
// 请求结构,1 表示序号
message HelloRequest {
string name = 1;
uint32 age = 2;
}
// 响应结构
message HelloResponse {
string msg = 1;
}
service Hello {
// 定义一个 Say 方法
rpc Say(HelloRequest) returns (HelloResponse) {}
}
|
然后:通过以下两个命令生成代码
1
2
3
|
# 先进入 proto 目录,然后执行下面命令
$ protoc --go_out=. hello.proto
$ protoc --go-grpc_out=. hello.proto
|
重复生成会覆盖,如果有新的更改可以重新执行命令
注意:生成的文件不要去更改,只需要重写业务部分即可,比如上面的例子重写 Say
方法就好了
最后:再复制一份 hello.proto
和生成的文件到 client
就好了
11.3.3 完善代码
编写服务端代码
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
|
// main.go
package main
import (
"context"
"fmt"
pb "gRPCDemo/server/apis/hello"
"google.golang.org/grpc"
"net"
)
// 集成还未实现的 Server
type server struct {
pb.UnimplementedHelloServer
}
// Say 实现
func (s *server) Say(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
fmt.Println("请求成功, 马上返回," + req.GetName())
return &pb.HelloResponse{Msg: "请求成功, " + req.GetName()}, nil
}
func main() {
// 1、开启一个端口
listen, err := net.Listen("tcp", ":9800")
if err != nil {
fmt.Println("端口启动失败")
return
}
// 2、创建 grpc 服务
grpcServer := grpc.NewServer()
// 3、注册服务到 grpcServer
pb.RegisterHelloServer(grpcServer, &server{})
// 4、启动服务
err = grpcServer.Serve(listen)
if err != nil {
fmt.Println("服务启动失败")
return
}
}
|
编写客户端代码
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
|
package main
import (
"context"
"fmt"
pb "gRPCDemo/client/apis/hello"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 1、连接到 Server,先不进行安全验证
conn, err := grpc.Dial("127.0.0.1:9800", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return
}
defer func(conn *grpc.ClientConn) { // 这里压栈一个匿名自调用函数,处理连接关闭
err := conn.Close()
if err != nil {
fmt.Println("连接关闭异常")
}
}(conn)
// 2、建立连接
client := pb.NewHelloClient(conn)
// 3、执行 rpc 调用
response, err := client.Say(context.Background(), &pb.HelloRequest{Name: "Bob"})
if err != nil {
fmt.Println("rpc 调用失败")
return
}
fmt.Println(response.GetMsg())
}
|
先启动服务端,再启动客户端,运行示例:
11.3.4 添加 SSL/TLS 认证
首先:电脑上安装证书生成工具
-
官网下载地址
:只有二进制压缩包版本,windows 不建议使用这个,需要自己编译
-
windows 安装包版本
:安装完如果没有自动添加环境变量,需要手动配置,执行
openssl
测试安装是否成功
然后:在服务端新建目录 /key
,进入该目录,生成证书(假设这个目录是第三方)。这里下载的是 v3
版本,所以下面的是 v3_req
v3_ca
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# 1、生成私钥
$ openssl genrsa -out server.key 2048
# 2、生成证书,全部回车即可,信息可以不填
$ openssl req -new -x509 -key server.key -out server.crt -days 36500
# 3、生成 csr
$ openssl req -new -key server.key -out server.csr
# 4、拷贝 openssl 安装目录下的 /bin/conf/openssl.cnf 文件到 /key 目录。linux 下是 openssl.cfg
# 找到 [# copy_extensions = copy],[# req_extensions = v3_req] 去掉 #
# 找到 [[ v3_ca ]] 在其前面添加下面的代码,配置的域名为证书允许的访问域名
subjectAltName = @alt_names
[ @alt_names ]
DNS.1 = mydomain.com
# 5、生成证书私钥
$ openssl genpkey -algorithm RSA -out test.key
# 6、通过证书私钥生成 csr
$ openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req
# 7、生成 SAN 证书 pem,公钥可以通过这个解析到
$ openssl x509 -req -days 3650 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
|
最后:在服务端和服务端添加认证
服务端
1
2
3
4
5
6
7
8
9
10
11
12
|
/* add: 添加证书,相对路径会加载失败 */
tlsFile, err := credentials.NewServerTLSFromFile(
"D:\\StudyFiles\\Go\\gRPCDemo\\key\\test.pem",
"D:\\StudyFiles\\Go\\gRPCDemo\\server\\key\\test.key",
)
if err != nil {
fmt.Println("证书加载失败")
return
}
// ...
/* alter: 创建 grpc 服务,传入证书 */
grpcServer := grpc.NewServer(grpc.Creds(tlsFile))
|
客户端
1
2
3
4
5
6
7
8
9
10
11
12
|
/* add: 添加证书,相对路径会加载失败 */
tlsFile, err := credentials.NewClientTLSFromFile(
"D:\\StudyFiles\\Go\\gRPCDemo\\key\\test.pem",
"mydomain.com",
)
if err != nil {
fmt.Println("证书加载失败")
return
}
/* 1、alter: 连接到 Server, 传入证书 */
conn, err := grpc.Dial("127.0.0.1:9800", grpc.WithTransportCredentials(tlsFile))
|
11.3.5 Spring Boot 与 Go gRPC 实践
先把上一步证书添加的代码去掉,回到没有证书之前的代码
再将客户端访问路径切换成一个新的端口请求路径——Spring Boot 注册的端口
1
|
conn, err := grpc.Dial("127.0.0.1:9801", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
打包 client 和 server,放到服务器上
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
<!-- parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.41.0</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.21.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- build -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<!--增加jvm参数-->
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
|
- 编写 proto 文件:
src/main/resources/proto/hello.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 语法格式
syntax = "proto3";
// 生成的 go 代码目录和包名
// ; 前表示生成代码的相对目录,; 后表示生成代码的包名
option java_package = ".;org.example.grpc"; // 注意这里从 go_package 改为了 java_package
// 请求结构,1 表示序号
message HelloRequest {
string name = 1;
uint32 age = 2;
}
// 响应结构
message HelloResponse {
string msg = 1;
}
service Hello {
// 定义一个 Say 方法
rpc Say(HelloRequest) returns (HelloResponse) {}
}
|
1
|
$ protoc --proto_path=src/main/resources/proto --java_out=src/main/java --grpc-java_out=src/main/java hello.proto
|
- 配置
application.properties
文件
1
2
3
4
5
|
grpc.server.port=9802
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=true
server.servlet.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8
|
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
|
@SpringBootApplication
public class GrpcApplication {
public static void main(String[] args) {
SpringApplication.run(GrpcApplication.class, args); // 启动 java grpc 服务
invokeGRPC(); // 直接发送请求调用 go grpc 服务
}
/**
* invokeGRPC 调用 Go gRPC 服务
*/
public static void invokeGRPC() {
// 1、创建连接通道
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 9800)
.usePlaintext()
.build();
System.out.println("创建连接通道");
// 2、绑定客户端
HelloGrpc.HelloBlockingStub stub = HelloGrpc.newBlockingStub(channel);
// 3、创建请求
HelloOuterClass.HelloRequest request = HelloOuterClass.HelloRequest.newBuilder().setName("Java gRPC").build();
// 4、发送请求
HelloOuterClass.HelloResponse response = stub.say(request);
System.out.println(response.getMsg());
// 5、关闭通道
channel.shutdown();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@GrpcService
public class HelloService extends HelloImplBase {
@Override
public void say(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
// 1、拼装返回数据
String msg = "请求成功," + request.getName();
// 2、创建返回结果
HelloOuterClass.HelloResponse response = HelloResponse.newBuilder().setMsg(msg).build();
// 3、返回请求
responseObserver.onNext(response);
// 4、完成
responseObserver.onCompleted();
}
}
|
11.4 gin + gRPC 实践
11.4.1 目录结构
Golang 约定的目录结构
Golang 的目录结构约定来源于社区,以下为单个项目目录结构,不建议有 src
目录,区分 Java、JavaScript 等
来源:
github——project-layout
。star = 38k
1
2
3
4
5
6
7
8
9
10
11
12
13
|
├── cmd # 可执行文件,可能包含多个 main 文件
├── internal # 内部代码,不希望外部访问
├── pkg # 公开代码,外部可以访问
├── config/configs/etc # 配置文件
├── scripts # 脚本
├── docs # 文档
├── third_party # 第三方工具
├── bin # 二进制文件
├── build # 持续集成相关
├── deploy # 部署相关
├── test # 测试文件
├── api # 开发的 api 接口
├── init # 初始化
|
实践项目目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
├── common # 公共代码目录
│ ├── errs # 自定义错误
│ ├── R # 接口返回结构
│ ├── run # 服务启动函数
│ ├── utils # 通用工具类
├── config # 项目配置
├── grpc # proto 文件和生成的 grpc 代码
├── server-user # user 服务目录
│ ├── api # 接口目录
│ ├── constants # 常量
│ ├── grpc # grpc 服务端实现
│ ├── router
│ └── grpc.go # grpc 服务注册文件
│ └── router.go # gin 路由注册文件
│ └── main.go # 服务入口文件
|
11.4.2 安装依赖包和命令
1
2
3
4
5
6
7
8
9
10
11
|
# gin
$ go get -u github.com/gin-gonic/gin
# grpc
$ go get google.golang.org/grpc
# viper 配置包
$ go github.com/spf13/viper
# grpc 代码生成
$ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc # 需要先拉取
# grpc 代码生成命令
$ go install google.golang.org/protobuf/cmd/protoc-gen-go # 生成 go 代码命令
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc # 生成 go grpc 代码命令
|
11.4.3 编写公共代码
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
|
package errs
import (
"fmt"
codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type ErrorCode int
type MyError struct { // 自定义 error
Code ErrorCode
Msg string
}
func (this *MyError) Error() string { // 实现 error 接口
return fmt.Sprintf("[Error] code is %v, %s", this.Code, this.Msg)
}
func NewError(code ErrorCode, msg string) *MyError {
return &MyError{code, msg}
}
func GrpcError(err *MyError) error { // 自定义 error 转 grpc error
return status.Error(codes.Code(err.Code), err.Msg)
}
func ParseGrpcError(err error) (ErrorCode, string) { // 获取 grpc error 的 code 和 message
fromError, _ := status.FromError(err)
return ErrorCode(fromError.Code()), fromError.Message()
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package R
import "net/http"
type R[T any] struct {
Status int `json:"status"`
Code int `json:"code"`
Success bool `json:"success"`
Data T `json:"data"`
}
func Success[T any](data T) *R[T] {
return &R[T]{Status: http.StatusOK, Code: http.StatusOK, Success: true, Data: data}
}
func Fail(code int, data string) *R[string] {
return &R[string]{Status: http.StatusOK, Code: code, Success: false, Data: data}
}
|
1
2
3
4
5
6
7
8
9
|
package utils
import "regexp"
// CheckMobile 检验手机号
func CheckMobile(phone string) bool {
reg := regexp.MustCompile("^1[345789]+\\d{9}$")
return reg.MatchString(phone)
}
|
/server-user/constants/global.go
1
2
3
4
5
6
|
package constants
const (
ServerName = "server-user"
GrpcServerName = "grpc-user"
)
|
11.4.4 编写启动文件
新建文件 common/run/run.go
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
45
|
package run
import (
"context"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func Run(port, serverName string, handle *gin.Engine, stopGrpc func()) {
// 1、开启一个服务端口
srv := &http.Server{Addr: port, Handler: handle}
// 2、启动一个携程监听服务状态,正常打印启动成功,异常打印异常信息
go func() {
log.Printf("%s running in %s", serverName, port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalln(err)
}
}()
// 3、创建一个信号量通道:监听服务停止
quit := make(chan os.Signal)
// SIGINT: ctrl + c 信号
// SIGTERM: 程序结束信号
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Printf("Shutting down server %s...", serverName)
// 4、开始停止服务,启动上下文监听器:2s 后输出服务停止信息
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil { // 退出 gin 服务
log.Fatalln("Failed to shutdown, cause by: ", err)
}
if stopGrpc != nil { // 退出 grpc 服务
stopGrpc()
}
select {
case <-ctx.Done():
log.Println("Waiting...")
}
log.Println("server stop success!")
}
|
/server-user/main.go
启动服务
1
2
3
4
5
6
7
8
9
10
11
|
package main
import (
"gin-grpc-demo/common/run"
"github.com/gin-gonic/gin"
)
func main() {
gs := gin.Default()
run.Run(":3003", "server-user", gs, nil)
}
|
11.4.5 路由注册
- 编写路由注册文件
/server-user/router/router.go
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
|
package router
import (
"github.com/gin-gonic/gin"
)
type Router interface { // 定义接口
Route(handel *gin.Engine)
}
type Register struct { // 定义类(抽象)
}
func (*Register) Route(r Router, gs *gin.Engine) { // Register 类实现 Router 接口(会被重载)
r.Route(gs)
}
// 路由表
var routes []Router
func InitRouter(gs *gin.Engine) { // 注册路由表中的路由。main.go 调用
for _, route := range routes {
route.Route(gs)
}
}
func AddRoute(rs ...Router) { // 添加路由到路由表。api 目录调用
routes = append(routes, rs...)
}
|
- 新建
/server-user/api/api.go
/server-user/api/user/route.go
/server-user/api/user/user.go
1
2
3
4
5
6
|
/* api.go */
package api
import (
_ "gin-grpc-demo/server-user/api/user"
)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/* api/user/user.go */
package user
import (
"github.com/gin-gonic/gin"
"net/http"
)
type HandleUser struct {
}
func (*HandleUser) getCaptcha(ctx *gin.Context) {
ctx.JSON(http.StatusOK, R.Success("654321"))
}
func (*HandleUser) getCaptchaGrpc(ctx *gin.Context) {
// grpc 调用: 待实现
}
|
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
|
/* api/user/route.go */
package user
import (
"gin-grpc-demo/server-user/router"
"github.com/gin-gonic/gin"
"log"
)
const (
BaseUser = "/user"
)
func init() {
log.Println("init user router")
router.AddRoute(&RouterUser{})
}
type RouterUser struct {
router.Router
}
func (*RouterUser) Route(gs *gin.Engine) { // 实现 Route 方法
handle := &HandleUser{}
gs.GET(BaseUser+"/login/captcha", handle.getCaptcha)
gs.POST(BaseUser+"/login/captchaGrpc", handle.getCaptchaGrpc)
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* /server-user/main.go */
package main
import (
"gin-grpc-demo/common/run"
// 匿名导入全部 api。如果没有将不会打包 api 目录下的文件,也就无法被注册
_ "gin-grpc-demo/server-user/api"
"gin-grpc-demo/server-user/router"
"github.com/gin-gonic/gin"
)
func main() {
gs := gin.Default()
router.InitRouter(gs) // 注册路由
run.Run(":3003", "server-user", gs, nil)
}
|
启动服务,使用 postman/apifox 可以访问 GET localhost:3003/user/login/captcha
11.4.6 项目配置文件(viper)
1
|
$ go get github.com/spf13/viper
|
- 新建配置文件
config/config.yaml
1
2
3
4
5
|
servers:
server-user: # 服务名
addr: "localhost:3003" # 启动地址
grpc-user:
addr: "localhost:3004"
|
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package config
/* https://github.com/spf13/viper */
import (
"github.com/spf13/viper"
"log"
"os"
)
type ServerConfig struct { // 服务配置
Name string
Addr string
}
type Config struct { // 全局配置
viper *viper.Viper
srvCons map[string]*ServerConfig
}
// GConfig 全局配置指针变量
var GConfig *Config
func Init() *Config { // 初始化 yaml 配置
log.Println("========== Reading Config ===========")
defer log.Println("========== Read Config Success ===========")
if GConfig != nil { // 已加载直接返回
return GConfig
}
GConfig = &Config{viper: viper.New(), srvCons: make(map[string]*ServerConfig)}
dir, err := os.Getwd()
if err != nil {
log.Fatalln("Get workDir error: ", err)
}
GConfig.viper.SetConfigName("config")
GConfig.viper.SetConfigType("yaml")
GConfig.viper.AddConfigPath(dir + "/config")
if err = GConfig.viper.ReadInConfig(); err != nil {
log.Fatalln("Read config error: ", err)
}
return GConfig
}
// ReadServerConfig method: 读取服务的配置
func (this *Config) ReadServerConfig(name string) *ServerConfig {
if this.srvCons[name] != nil { // 如果已经加载配置,直接返回
return this.srvCons[name]
}
sc := &ServerConfig{}
serverMap := this.viper.GetStringMapString("servers." + name)
if serverMap["addr"] == "" {
log.Printf("[Error] Cannot get [addr] config from server: %s, check config.yaml\n", name)
panic(name)
}
sc.Name = name
sc.Addr = serverMap["addr"]
this.srvCons[name] = sc
return sc
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var globalConfig *config.Config
var serverConfig *config.ServerConfig
func init() { // 初始化配置
globalConfig = config.Init()
serverConfig = globalConfig.ReadServerConfig(constants.ServerName)
}
func main() {
gs := gin.Default()
router.InitRouter(gs) // 注册路由
common.Run(serverConfig.Addr, serverConfig.Name, gs, nil)
}
|
11.4.5 引入 gRPC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
syntax = "proto3";
option go_package = ".;grpcUser";
// import "google/protobuf/any.proto";
message CaptchaRequest {
string phone = 1;
}
message CaptchaResponse {
string data = 1;
// google.protobuf.Any data = 1;
}
service User {
rpc GetCaptcha(CaptchaRequest) returns (CaptchaResponse) {}
}
|
1
2
|
$ cd ./grpc/user
$ protoc --go_out=. --go-grpc_out=. user.proto
|
- 实现 gRPC 服务端
/server-user/user/grpc.user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package user
import (
"context"
"gin-grpc-demo/common/errs"
"gin-grpc-demo/common/utils"
"gin-grpc-demo/grpc/user"
)
type GrpcUserServer struct {
grpcUser.UnimplementedUserServer
}
func (*GrpcUserServer) GetCaptcha(ctx context.Context, req *grpcUser.CaptchaRequest) (*grpcUser.CaptchaResponse, error) {
phone := req.GetPhone()
if phone == "" {
return nil, errs.GrpcError(errs.NewError(2001, "空的手机号"))
}
if !utils.CheckMobile(phone) {
return nil, errs.GrpcError(errs.NewError(2002, "非法的手机号"))
}
res := &grpcUser.CaptchaResponse{Data: "123456"}
return res, nil
}
|
- 实现 gRPC 客户端
/server-user/api/user/user.go
中的 getCaptchaGrpc
方法
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
|
package user
import (
"gin-grpc-demo/common/R"
"gin-grpc-demo/common/errs"
"gin-grpc-demo/config"
grpcUser "gin-grpc-demo/grpc/user"
"gin-grpc-demo/server-user/constants"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"net/http"
)
type HandleUser struct {
}
func (*HandleUser) getCaptcha(ctx *gin.Context) {
ctx.JSON(http.StatusOK, R.Success("654321"))
}
func (*HandleUser) getCaptchaGrpc(ctx *gin.Context) { // 实现 grpc 客户端调用
phone := ctx.PostForm("phone") // 请求参数
grpcConfig := config.GConfig.ReadServerConfig(constants.GrpcServerName) // grpc 服务端配置
conn, err := grpc.Dial(grpcConfig.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Println("[Error] invalid credentials, ", err)
}
userClient := grpcUser.NewUserClient(conn)
res, err2 := userClient.GetCaptcha(ctx, &grpcUser.CaptchaRequest{Phone: phone})
if err2 != nil {
code, msg := errs.ParseGrpcError(err2) // 错误解析
ctx.JSON(http.StatusOK, R.Fail(int(code), msg)) // 返回错误信息
return
}
ctx.JSON(http.StatusOK, R.Success(res.Data)) // 请求成功
}
|
- 注册 gRPC 服务
/server-user/router/grpc.go
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
|
package router
import (
"gin-grpc-demo/config"
"gin-grpc-demo/grpc/user"
"gin-grpc-demo/server-user/constants"
"gin-grpc-demo/server-user/grpc/user"
"google.golang.org/grpc"
"log"
"net"
)
func RegisterGrpc() *grpc.Server {
serverConfig := config.GConfig.ReadServerConfig(constants.GrpcServerName)
server := grpc.NewServer()
grpcUser.RegisterUserServer(server, &user.GrpcUserServer{})
listen, err := net.Listen("tcp", serverConfig.Addr)
if err != nil {
log.Printf("[Error] %s cannot listen. %s", serverConfig.Addr, err)
}
log.Printf("%s will running in %s\n", serverConfig.Name, serverConfig.Addr)
go func() {
defer log.Printf("Shutting down server %s...\n", serverConfig.Name)
err = server.Serve(listen) // 没有被调grpc服务时,这里会阻塞,所以需要放到携程
if err != nil {
log.Printf("[Error] server %s start error. %s", serverConfig.Name, err)
return
}
}()
return server // 这里返回是因为 gin 服务停止时需要停止 grpc 服务
}
|
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
|
/* /server-user/main.go */
package main
import (
"gin-grpc-demo/common/run"
"gin-grpc-demo/config"
"gin-grpc-demo/server-user/constants"
// 匿名导入全部 api。如果没有将不会打包 api 目录下的文件,也就无法被注册
_ "gin-grpc-demo/server-user/api"
"gin-grpc-demo/server-user/router"
"github.com/gin-gonic/gin"
)
var globalConfig *config.Config
var serverConfig *config.ServerConfig
func init() {
globalConfig = config.Init()
serverConfig = globalConfig.ReadServerConfig(constants.ServerName)
}
func main() {
gs := gin.Default()
router.InitRouter(gs) // 注册路由
gc := router.RegisterGrpc() // 注册 grpc 服务
stopGrpc := func() { gc.Stop() }
run.Run(serverConfig.Addr, serverConfig.Name, gs, stopGrpc)
}
|
使用 postman/apifox 调用
localhost:3003/user/login/captcha
: 将会正常调用 gin 服务
localhost:3003/user/login/captchaGrpc
: 将会通过 gin 调用 gRPC 服务
11.5 更多工程实践
本文内容已过多,其他工程实践后续将会另起文档