Featured image of post 万字速通 Golang

万字速通 Golang

含代码和工程实践过程

Golang 总结

golang-feature

环境搭建

  • 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 命名约定

  1. 严格区分大小写
  2. package main 包为程序入口文件,有且必须唯一。func main 函数为每个文件的入口函数。
  3. 包名和文件名不要求一致,但尽量保持一致,且不与标准库冲突。程序自定义的包通过路径导入
  4. 不允许未使用的变量,存在则编译不通过
  5. 变量、常量、函数名建议 驼峰命名
  6. 约定大写字母开头的可以被包外部使用,小写字母开头的只能包内部使用

2. 数据类型

Golang数据类型

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. 不同类型的数据无法进行运算,编译不通过

      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 的长度(已使用的内存空间),这取决于具体类型

    1. 数组:v中元素的数量
    2. 数组指针:*v中元素的数量(v为nil时panic)
    3. 切片、映射:v中元素的数量;若v为nil,len(v)即为零
    4. 字符串:v中字节的数量
    5. 信道:信道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零
  • func cap(v Type) int:返回 v 的容量(分配的内存空间),这取决于具体类型

    1. 数组:v中元素的数量,与 len(v) 相同
    2. 数组指针:*v中元素的数量,与len(v) 相同
    3. 切片:切片的容量(底层数组的长度);若 v为nil,cap(v) 即为零
    4. 信道:按照元素的单元,相应信道缓存的容量;若v为nil,cap(v)即为零
  • func make(Type, size IntegerType) Type:分配并初始化一个类型为切片、映射、或通道的对象

    1. 切片:size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,容量为10的切片。
    2. 映射:初始分配的创建取决于size,但产生的映射长度为0。size可以省略,这种情况下就会分配一个小的起始大小。
    3. 信道:信道的缓存根据指定的缓存容量初始化。若 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 需要注意的点:

  1. map 是无序的,无法进行有序遍历
  2. 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

在上面的例子中,如果没有关闭操作,两种情况将会造成阻塞死锁

  1. 信道为空,子线程不再写入数据:主线程将死锁在信道读取
  2. 信道为满,主线程不再读取数据:子线程将死锁在信道写入

因此,我们常常需要手动关闭信道的操作,让程序继续正常运行

  • 信道的遍历(range)

在上面的例子中,通过 普通 for 循环 多循环两次,得到的结果是空,其实是没必要的

我们可以使用 增强 for 循环 去遍历。

  1. 信道为空且关闭的时候,自动结束循环
  2. 信道为空未关闭的时候,阻塞等待
1
2
3
4
// 将上面循环读取的代码改为下面的内容
for val := range c {
    fmt.Printf("main task running, get = %c\n", val)
}
  • 多路信道的监控(select)
 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
}

如果查看源码可以发现,基本数据类型中的 runeint32 的别名,byteunit8 的别名

2.2.9 函数

3. 运算符

如果有其他语言的基础,以下列举了 JavaJavaScript 中的主要差异

细节方面可以自行了解,然后这一章节可以直接跳过

  • Java:

    1. Golang 中没有三目运算符(? :)、取反运算符(~
    2. Golang 中多了两个 指针运算符 &<name> *<name>,一个 信道操作符 <-
  • JavaScript:

    1. 在与 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 循环

  • 普通 for 循环
 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++
	}
}
  • range for 循环

场景:如果字符串中含中文,用普通 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. 函数

注意事项:

  1. 函数也是一种数据类型,可以赋值给一个变量,也可作为另一个函数的形参
  2. 函数不支持重载
  3. 基本数据类型和数组:形参是值传递,对形参的值更改不会影响函数外部变量的值
  4. 如果形参传递的是指针,那么会影响函数外部变量的值

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 不会被释放

闭包产生的原因:

  1. 存在函数嵌套
  2. 函数内部的函数,引用了外部函数定义的变量
  3. 内部函数被调用

**闭包的本质:**本质是一个函数,只是引用了外部函数定义的变量

闭包的应用场景:

  1. 状态持久化的执行场景:如统计
  2. 时间相关的执行场景:如动画、节流防抖
  3. 函数内部状态的获取:如回调

**闭包的缺陷:**内存占用

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 包的使用

  1. 每个 .go 文件,开头必须使用 package 关键字声明包名

  2. 同个文件夹下的所有 .go 文件属于同一个包,因此包名需保持一致

  3. package main 为入口包,一个程序必须含有。func main() 为入口函数,每个文件必须含有

  4. 包名可以随意定义,但是建议和文件名一致,且不要和标准库冲突

  5. 包导入可以使用路径导入,可以是绝对路径或相对路径。相对路径从 $GOPATH/src 开始,其中 GOPATH 为系统环境变量(已废弃)。绝对路径和默认相对路径对开发并不友好,可移步 模块化

  6. 导入多个包建议以下写法

    1
    2
    3
    4
    
    import (
    	"fmt"
        "strconv"
    )
    
  7. 包导入可以定义别名。定义后,在当前文件,被导入的包名不能再使用

    1
    2
    3
    4
    5
    6
    7
    8
    
    import (
    	"fmt"
        pstr "strconv"
    )
    
    func main() {
        pstr.FormatInt("100") // 而不再是 strconv.FormatInt()
    }
    
  8. 调用包中定义的内容需要 包名. 开始

    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 结构,和 interfacepair 传递

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 标准包,满足以下条件的 属性或方法,可以被反射获取

  1. 大写开头的属性
  2. 大写开头的方法(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 中序列化需要通过反射获取字段名和值,但是获取字段需要大写
    1. 如果小写,序列化属性将会丢失
    2. 如果大写,不满足需求
  • 方案:通过属性标签,指定被序列化的 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 模型

goroutine

  1. 内核线程(M):负责多线程处理
  2. 线程控制器(P):连接线程与 goroutine 调度器,从 goroutine 调度器中取出资源(G),交给线程处理
  3. goroutine 调度器(GM):负责资源(G)的统一管理,根据 P 的需要,提供资源(G)
  4. 全局队列(GQ):空闲的资源在全局队列中,并且加锁。对全局队列的读取需要解锁(比较耗时)
  5. goroutine(G):线程最终需要的计算机资源

注:为方便后续内容描述,后面的将使用括号内的简写。如 goroutine 将直接描述为:G

8.2 多线程调度策略

  1. 复用线程

    1. 偷取(work stealing)
      • 场景:如模型图,假设P3(最右边的P) 本地队列为空,P2 中含 G1 且等待中;M2 请求 G1
      • 策略:P3 将从 M2 偷取 G1,然后交给 M2
    2. 握手(hand off)
      • 场景:如模型图,假设P3(最右边的P) 本地队列含 G1、G2;M2 请求 G1,G1阻塞,G2马上要被执行;此时 M3空闲
      • 策略:P3 将 G1 交给 M2继续等待,M3 与 P3 握手
  2. 利用并行:指定多个 P(一般为 CPU 核数的一半)

  3. 抢占:CPU 处理 G 耗时过长(约 10ms),G 将被新的 CPU 线程抢占

  4. 全局队列:如果所有的 P 中都没有 M 需要的 G,将会尝试从 GQ 中解锁获取

现在,可以回到 信道章节 查看基本使用

9. 模块化

go modules 需要 go@1.11 以上,淘汰原先的 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 文件

该文件作用:统一管理直接或间接依赖的所有模块版本,避免被篡改

  • 手动更改依赖的版本
    1. 直接修改 go.mod 文件
    2. 执行 go mod edit -replace=<cur>=<tar>,版本重定向

10. Go 生态

此章节介绍的所有 go 开源库和框架的 star,记录于 2022-2-21

10.1 Web 框架

  1. Beego:国内框架,自带 ORM 差点意思,star = 29.4k
  2. Gin:国外轻量级框架,star = 66.6k
  3. echo:国外轻量级框架,star = 25k
  4. Iris:国外框架,star = 23.6k

10.2 微服务框架

  1. go zero:集成了各种工程实践的 web 和 rpc 框架,star = 22.8k
  2. Go kit:较轻的微服务框架,集成各种 web 框架,最后更新时间 2021-09star = 24.6k
  3. Istio:较全的微服务框架,star = 32.4k

10.3 容器

  1. Kubernetes(k8s)
  2. Docker Swarm

10.4 服务发现

  1. Consul

10.5 存储引擎

  1. etcd : 分布式 k/v 存储 star = 42.6k
  2. tidb : 分布式 关系数据库,中文文档 star = 33.5k
  3. vitess :分布式 MySQL,中文文档 star = 15.6k

10.6 关系映射

  1. xorm:star = 6.6k
  2. gorm:star = 31.5k

10.7 日志&配置

  1. logrus:star = 22.2k

  2. zap:star = 18.2k

  3. viper:star = 22.1k

10.8 权限

  1. casbin:star = 13.9k

10.9 工作流

  1. cadence:star = 6.7k

11. web 实践

11.1 开发工具

假设已安装 go 在电脑上,并已配置好全局变量、开启 go modules

  • 如果是学习基本语法等,推荐使用 VS Code,你只需要下载安装后,安装下面三个插件
    1. Auto Import
    2. Go 或者 Go Nightly
    3. Chinese(汉化插件,可选)
  • 如果是项目开发,推荐使用 Goland
    1. 打开 Goland -> Settings -> Go
    2. 将以下三个配置为与 go env 列出的一致

GOROOT

GOMODULES

GOPATH

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;
}
  • 加载 html 模板,加载静态资源,启动服务
 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 的执行程序到你的电脑,没有安装程序,直接解压并配置好环境变量

注意:下载的时候一定要找对版本,迭代的版本非常多

  • 正确的文件格式长这样 protoc-21.10-win64.zip,只是中间的版本会有不同

  • 控制台输入 protoc 验证是否安装成功

  • https://github.com/protocolbuffers/protobuf-go 这个

然后:准备好后新建一个 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          

最后:安装好相关插件,支持代码提示,高亮等

goland-proto-plugin

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())
}

先启动服务端,再启动客户端,运行示例:

grpc-success

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 实践

  • Go gRPC 打包

先把上一步证书添加的代码去掉,回到没有证书之前的代码

再将客户端访问路径切换成一个新的端口请求路径——Spring Boot 注册的端口

1
conn, err := grpc.Dial("127.0.0.1:9801", grpc.WithTransportCredentials(insecure.NewCredentials()))

打包 client 和 server,放到服务器上

  • 安装 protoc-gen-grpc-java

    1. 前往 Maven 仓库
    2. 搜索 protoc-gen-grpc-java
    3. 选择版本,点击 files->view all
    4. 选择对应系统的可执行文件
    5. 下载放到电脑指定目录,并配置环境变量,这里下载的是 1.41.0
  • 创建一个 Spring Boot 项目,如果是 Maven,添加依赖

 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) {}
}
  • 项目根 目录,生成 Java gRPC 代码
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 编写公共代码

  • /common/errs/err.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
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()
}
  • /common/R/R.go
 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}
}
  • /common/utils/check.go
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"
  • 读取配置文件 config/config.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
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
}
  • 修改 server-user/main.go
 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

  • grpc/user/user.proto 文件
 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) {}
}
  • 生成 gRPC 代码
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 更多工程实践

本文内容已过多,其他工程实践后续将会另起文档

Licensed under CC BY-NC-SA 4.0
最后更新于 2023-03-22 20:33:00