Go
11. 协程 & 线程
11.1 协程
Gorutine是Go运行时管理的轻量级线程。在Go中,开启一个协程是非常简单的,使用go关键字可开启协程。
先看看串行执行结果
go
package main
import (
"fmt"
"time"
)
// 这里应使用Printf
func shopping(name string) {
fmt.Println("%s 开始购物", name)
time.Sleep(1 * time.Second)
fmt.Println("%s 购物结束", name)
}
// 协程
func main() {
startTime := time.Now()
// 串行执行
shopping("张三")
shopping("李四")
shopping("王五")
fmt.Println("购买完成")
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
}

使用go关键字开启协程,然而主线程结束,即协程结束。
go
package main
import (
"fmt"
"time"
)
// 这里应使用Printf
func shopping(name string) {
fmt.Println("%s 开始购物", name)
time.Sleep(1 * time.Second)
fmt.Println("%s 购物结束", name)
}
// 协程
func main() {
startTime := time.Now()
// 串行执行
go shopping("张三")
go shopping("李四")
go shopping("王五")
fmt.Println("购买完成")
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
}

故需要定义一个类似于计数信号量(Counting Semaphore)的wait变量,用于等待协程执行完毕,但结束时数值不增加。
go
package main
import (
"fmt"
"time"
)
var wait int
// 这里应使用Printf
func shopping(name string) {
fmt.Println("%s 开始购物", name)
time.Sleep(1 * time.Second)
fmt.Println("%s 购物结束", name)
wait--
}
// 协程
func main() {
startTime := time.Now()
wait = 3
go shopping("张三")
go shopping("李四")
go shopping("王五")
for {
if wait == 0 {
break
}
}
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
}

而官方对协程协同进行了相关结构体的定义

我们可以使用waitGroup代替我们此前的粗糙实现
go
package main
import (
"fmt"
"sync"
"time"
)
func shopping(name string, wait *sync.WaitGroup) {
fmt.Println("%s 开始购物", name)
time.Sleep(1 * time.Second)
fmt.Println("%s 购物结束", name)
wait.Done()
}
// 协程(不建议使用全局变量定义wait)
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
go shopping("张三", &wait)
go shopping("李四", &wait)
go shopping("王五", &wait)
wait.Wait()
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
}

其具有更加复杂的实现:

11.2 Channel
Channel允许将数据传递给其他协程函数及主线程。
go
package main
import (
"fmt"
"sync"
"time"
)
var moneyChannel = make(chan int)
func pay(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChannel <- money
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
go pay("张三", 3, &wait)
go pay("李四", 4, &wait)
go pay("王五", 5, &wait)
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel)
wait.Wait()
}()
var moneyList []int
for {
// money := <-moneyChannel
money, ok := <-moneyChannel
moneyList = append(moneyList, money)
fmt.Println(money, ok)
if !ok {
break
}
}
wait.Wait()
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
fmt.Println(moneyList)
}
第4个协程非常关键,其可以用于监听是否结束,否则程序会出现死锁:
go
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel)
wait.Wait()
}()

利用forr对for循环进行精简
go
//for {
// // money := <-moneyChannel
// money, ok := <-moneyChannel
// moneyList = append(moneyList, money)
// fmt.Println(money, ok)
// if !ok {
// break
// }
//}
// 以上for循环精简写法
for money := range moneyChannel {
moneyList = append(moneyList, money)
fmt.Println(money)
}

11.3 Select & 超时处理
11.3.1 Select
go
package main
import (
"fmt"
"sync"
"time"
)
var moneyChannel1 = make(chan int)
var nameChannel1 = make(chan string)
func send(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChannel1 <- money
nameChannel1 <- name
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
go send("张三", 3, &wait)
go send("李四", 4, &wait)
go send("王五", 5, &wait)
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel1)
defer close(nameChannel1)
wait.Wait()
}()
var moneyList []int
var nameList []string
// 再开一个协程函数,负责nameChannel1循环
go func() {
for name := range nameChannel1 {
nameList = append(nameList, name)
fmt.Println(name)
}
}()
// 主协程函数,负责moneyChannel1循环
for money := range moneyChannel1 {
moneyList = append(moneyList, money)
fmt.Println(money)
}
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
fmt.Println(moneyList)
fmt.Println(nameList)
}

这样可以完成,但当Channel数量增多时难以管理,所以利用select对其进行简化。
go
package main
import (
"fmt"
"sync"
"time"
)
var moneyChannel1 = make(chan int)
var nameChannel1 = make(chan string)
func send(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChannel1 <- money
nameChannel1 <- name
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
go send("张三", 3, &wait)
go send("李四", 4, &wait)
go send("王五", 5, &wait)
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel1)
defer close(nameChannel1)
wait.Wait()
}()
var moneyList []int
var nameList []string
for {
select {
case money := <-moneyChannel1:
moneyList = append(moneyList, money)
case name := <-nameChannel1:
nameList = append(nameList, name)
}
}
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
fmt.Println(moneyList)
fmt.Println(nameList)
}

这样程序将无法停止,需要再增加一个Channel专用于关闭其他信道。
go
// ...
var doneChan = make(chan struct{}) // 声明一个用于关闭的信道
func main() {
// ...
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel1)
defer close(nameChannel1)
defer close(doneChan) // 增加
wait.Wait()
}()
var moneyList []int
var nameList []string
for {
select {
case money := <-moneyChannel1:
moneyList = append(moneyList, money)
case name := <-nameChannel1:
nameList = append(nameList, name)
case <-doneChan: // 一个不接收值专用于监测的数值
// break无法结束,需使用return,故将此后代码移至此处
fmt.Println("结束")
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
fmt.Println(moneyList)
fmt.Println(nameList)
return
}
}
}
再次定义内部函数,将结束代码移出。
go
package main
import (
"fmt"
"sync"
"time"
)
var moneyChannel1 = make(chan int)
var nameChannel1 = make(chan string)
var doneChan = make(chan struct{}) // 声明一个用于关闭的信道
func send(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChannel1 <- money
nameChannel1 <- name
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
go send("张三", 3, &wait)
go send("李四", 4, &wait)
go send("王五", 5, &wait)
// 再开一个协程函数,监听是否结束
go func() {
defer close(moneyChannel1)
defer close(nameChannel1)
defer close(doneChan)
wait.Wait()
}()
var moneyList []int
var nameList []string
var event = func() {
for {
select {
case money := <-moneyChannel1:
moneyList = append(moneyList, money)
case name := <-nameChannel1:
nameList = append(nameList, name)
case <-doneChan:
return
}
}
}
event()
fmt.Println("结束")
timeSlice := time.Since(startTime)
fmt.Println(timeSlice)
fmt.Println(moneyList)
fmt.Println(nameList)
}

11.3.2 超时处理
go
package main
import (
"fmt"
"time"
)
var done = make(chan bool)
func event() {
fmt.Println("event开始执行")
time.Sleep(3 * time.Second)
fmt.Println("event执行完成")
done <- true
}
func main() {
go event()
select {
case <-done:
fmt.Println("协程执行完成")
case <-time.After(4 * time.Second):
fmt.Println("超时执行中止")
}
}

go
func event() {
fmt.Println("event开始执行")
time.Sleep(6 * time.Second)
fmt.Println("event执行完成")
done <- true
}

11.4 线程安全 & sync.Map
11.4.1 线程安全
两个协程同时触发,一个协程自增全局整数,另一个协程自减这个全局整数,那么结束后结果应当抵消为0。
但是普通定义结果并不如人意,其结果无法预测。
go
package main
import (
"fmt"
"sync"
)
var sum int
var wait sync.WaitGroup
func add() {
for i := 0; i < 100000; i++ {
sum++
}
wait.Done()
}
func sub() {
for i := 0; i < 100000; i++ {
sum--
}
wait.Done()
}
func main() {
wait.Add(2)
go add()
go sub()
wait.Wait()
fmt.Println(sum)
}


加锁可以解决这个问题
go
package main
import (
"fmt"
"sync"
)
var sum int
var wait sync.WaitGroup
var lock sync.Mutex
func add() {
lock.Lock()
for i := 0; i < 100000; i++ {
sum++
}
lock.Unlock()
wait.Done()
}
func sub() {
lock.Lock()
for i := 0; i < 100000; i++ {
sum--
}
lock.Unlock()
wait.Done()
}
func main() {
wait.Add(2)
go add()
go sub()
wait.Wait()
fmt.Println(sum)
}

11.4.2 sync.Map
sync.Map是一种线程安全的Map,类似于Java中的ConcurrentHashmap。
一般情况
go
package main
import "fmt"
var maps = map[int]string{}
func main() {
go func() {
for {
maps[1] = "张三"
}
}()
go func() {
for {
fmt.Println(maps[1])
}
}()
select {}
}

线程安全的Map可以接受任何类型数据

go
package main
import (
"fmt"
"sync"
)
func main() {
var maps = sync.Map{}
go func() {
for {
maps.Store(1, "张三")
}
}()
go func() {
for {
val, ok := maps.Load(1)
fmt.Println(val, ok)
}
}()
select {}
}

这种Map内部进行加锁,所以线程安全。


线程安全Map的常用方法,字面意思可以推断。

12. 异常处理 & 泛型
12.1 异常处理
Go语言异常处理没有捕获机制,所以每调一次都要接一次error。
12.1.1 向上传递
Go语言没有Java/Python语言的throw与try-catch机制,故而将error作为返回值返回。
将错误交给上一级处理:一般用于框架层,有些错误框架层面不能擅自做主,故而向上传递 (向上抛)。
go
package main
import (
"errors"
"fmt"
)
// 将error作为返回值返回
func parent() error {
err := method()
return err
}
func method() error {
return errors.New("出错了")
}
func main() {
fmt.Println(parent())
}

再举例解释说明"向上传递"的定义。
go
package main
import (
"errors"
"fmt"
)
func div(a, b int) (res int, err error) {
if b == 0 {
err = errors.New("除数不能为0")
return
}
res = a / b
return
}
func server() (res int, err error) {
res, err = div(2, 0)
if err != nil {
// 错误向上传递
return
}
// 执行其他逻辑
res++
res += 2
return
}
func main() {
res, err := server()
if err != nil {
// 在这里输出
fmt.Println("a haha")
fmt.Println(err)
return
}
fmt.Println(res)
}

12.1.2 中断程序
遇到错误直接停止程序:一般用于初始化,初始化失败将导致后续继续错误,故而中断停止。
go
package main
import (
"fmt"
"os"
)
func init() {
// 读取配置文件,路径错误
_, err := os.ReadFile("xxx")
if err != nil {
panic(err.Error())
}
}
func main() {
fmt.Println("main")
}

再举例解释说明"中断程序"的定义。
go
package main
import (
"fmt"
"log"
"os"
)
func init() {
_, err := os.ReadFile("11235")
if err != nil {
log.Fatalln("程序出错!")
// panic("程序出错")
}
}
func main() {
fmt.Println("main")
}

错误日志输出包含了程序退出:
go
log.Fatalln("程序出错!")

12.1.3 恢复程序
可以在函数中使用defer捕获panic,以至于出现错误不至于程序崩溃。
go
package main
import (
"fmt"
"runtime/debug"
)
func read() {
defer func() {
// recover() 告诉程序有无panic或异常情况
if err := recover(); err != nil {
fmt.Println(err) // 打印错误 但无法溯源 (1)
s := string(debug.Stack()) // 打印堆栈 以便于溯源 (2)
fmt.Println(s)
}
}()
var list = []int{2, 3}
fmt.Println(list)
}
func main() {
read()
}
程序继续执行,没有因此崩溃。

再举例解释说明"恢复程序"的定义。
go
package main
import "fmt"
func read() {
defer func() {
err := recover()
if err != nil {
fmt.Println(err) // 有错误则打印错误信息
}
}()
var list = []int{1, 2}
fmt.Println(list[2])
}
func main() {
read()
}

错误会向上抛,可以做最后"兜底"。
go
package main
import "fmt"
func read() {
var list = []int{1, 2}
fmt.Println(list[2])
}
func readOuter() {
fmt.Println("执行Outer")
read()
}
func service() {
fmt.Println("执行Service前")
defer func() {
err := recover()
if err != nil {
fmt.Println(err) // 有错误则打印错误信息
}
}()
readOuter()
// 因readOuter()出现read()传递的错误,service函数生命周期立刻结束,立即触发defer,故触及不到此处代码
fmt.Println("执行Service后")
}
func main() {
fmt.Println("main前")
service()
// service生命周期结束,但return回main,main继续执行
fmt.Println("main后")
}

12.2 泛型
Go语言在1.18版本开始后,添加了对泛型的支持,即类型参数。
12.2.1 泛型函数
没有泛型时,int类型通用的求和函数。
go
package main
func plus(a, b int) int {
return a + b
}
func plusUint(a, b uint) uint {
return a + b
}
func main() {
plus(1, 2)
var u1, u2 = uint(2), uint(3)
plusUint(u1, u2)
plus(int(u1), int(u2))
}
实现一个独立的数字类型通用的求和函数,同时限制不能出现字符串相加、布尔相加报错等情况,这时就需要用到泛型了
go
// [] 方括号负责指定泛型可能出现的类型
func plus[T int | uint | float32 | float64](a, b T) T {
return a + b
}
多类型泛型定义如下
go
// [] 多泛型由逗号分隔
func myFunc[T int, k string | int](a T, b K) {
}
Go语言中数字类型较多,定义接口则可以解决这个问题
go
type Number interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
func plus[T Number](a, b T) T {
return a + b
}
12.2.2 泛型结构体
go
package main
import (
"encoding/json"
"fmt"
)
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
func main() {
type User struct {
Name string `json:"name"`
}
type UserInfo struct {
Name string `json:"name"`
Age int `json:"age"`
}
userRes := Response{
Code: 0,
Msg: "成功",
Data: User{
Name: "用户A",
},
}
userInfoRes := Response{
Code: 0,
Msg: "成功",
Data: UserInfo{
Name: "用户A",
Age: 24,
},
}
byteUserData, _ := json.Marshal(userRes)
byteUserInfoData, _ := json.Marshal(userInfoRes)
fmt.Println(string(byteUserData))
fmt.Println(string(byteUserInfoData))
}
这样就创建了两个JSON
go
{"code":0,"msg":"成功","data":{"name":"用户A"}}
{"code":0,"msg":"成功","data":{"name":"用户A","age":24}}
经过反序列化后变成了Map
go
var userResNew Response
json.Unmarshal([]byte(`{"code":0,"msg":"成功","data":{"name":"用户A"}}`), &userResNew)
fmt.Println(userResNew)

这里需要对Response进行更改,添加泛型支持
go
type Response[T any] struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data T `json:"data"`
}
利用泛型即可将JSON中的实例进行对接
go
var userResNew Response[User]
json.Unmarshal([]byte(`{"code":0,"msg":"成功","data":{"name":"用户A"}}`), &userResNew)
fmt.Println(userResNew)

12.2.3 泛型数组
go
package main
type Arr [T any] []T
func main() {
var Arr Arr[string]
arr = append(Arr, "用户C")
var IntArr Arr[int]
AntArr = append(intArr, 2)
}
12.4 泛型Map
泛型Map中,K只能是基本数据类型。
go
package main
type DefMap [K string | int, V any] map[K]V
func main() {
var DefMap = make(DefMap[string, string])
DefMap["name"] = "用户C"
fmp.pringln(DefMap)
}
13. 文件操作
13.1 文件读取
13.1.1 单次读
go
package main
import (
"fmt"
"os"
)
func main() {
file, _ := os.ReadFile("go_study/hello.txt") // 第二个参数是error类型
fmt.Println(string(file)) // 读入byte数组,需要转换为string
}
读取文件内容如下:
你好,今天天气真不错!

13.1.2 分片读
适用于读取非常长的文件,一次读可能会非常卡顿。
go
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, _ := os.Open("go_study/hello.txt")
defer file.Close()
for {
buf := make([]byte, 1)
_, err := file.Read(buf)
if err == io.EOF {
break
}
fmt.Printf("%s", buf)
}
}
将原始文件换成英文字符,并重复6次,进行读取:
Hello! The weather is really nice today!Hello! The weather is really nice today!
Hello! The weather is really nice today!Hello! The weather is really nice today!
Hello! The weather is really nice today!Hello! The weather is really nice today!

13.1.3 缓冲读
按行读
go
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("go_study/hello.txt")
defer file.Close()
if err != nil {
panic(err)
}
buf := bufio.NewReader(file)
for {
line, _, err := buf.ReadLine()
if err == io.EOF {
break
}
fmt.Println(string(line))
}
}

指定分隔符
不指定则默认换行符为分隔符
go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("go_study/hello.txt")
defer file.Close()
if err != nil {
panic(err)
}
buf := bufio.NewScanner(file)
for buf.Scan() {
fmt.Println(buf.Text())
}
}
指定支持的分隔符

go
// ...
buf := bufio.NewScanner(file)
buf.Split(bufio.ScanWords) // 按照单词分隔
for buf.Scan() {
fmt.Println(buf.Text())
}
// ...

指定特定字符,则需要仿照其自行实现。
13.2 文件写入
13.2.1 单次写
go
package main
import (
"fmt"
"os"
)
func main() {
err := os.WriteFile("go_study/newFile.txt", []byte("写入完成"), os.ModePerm) // 第二个参数是error类型
fmt.Println(err)
}

13.2.2 文件打开方式
常见打开方式(组合)
go
// 如果文件不存在就创建
os.O_CREATE|os.O_WRONLY // RD=read WR=wirte
// 追加写
os.O_APPEND|os.WRONLY
// 可读可写
os.O_RDWR
所有打开方式

下面创建文件并写入
go
package main
import "os"
func main() {
file, err := os.OpenFile("go_study/abc.txt", os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
panic(err)
}
defer file.Close()
file.Write([]byte("ABCDEFG!"))
}

只写模式下,读取文件信息会报错
go
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.OpenFile("go_study/abc.txt", os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
panic(err)
}
defer file.Close()
file.Write([]byte("ABCDEFG!"))
byteData, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(byteData))
}

13.2.3 文件权限
Windows权限参数无效,主要用于Linux系统,在Windows下该参数会被无视,代表文件的模板和权限位。
三个占位符
- 第一个是文件所有者所拥有的权限
- 第二个是文件所在组对其拥有的权限
- 第三个是其他人对文件拥有的权限
根据UNIX系统权限模型,用三个比特位表示权限,并根据以下为基础求和表示:
shell
R:读Read,八进制为4
W:写Write,八进制为2
X:执行Execute,八进制为1
例如:
go
0444 表示三者均具只读权限
0666 表示三者均具读写权限
0777 表示三者均具读写执行权限
0764 表示所有者读写执行、组读写、其他只读
具体请参考Linux/UNIX的chmod命令。
13.3 文件复制
go
package main
import (
"fmt"
"io"
"os"
)
func main() {
rFile, err := os.Open("go_study/newFile.txt")
if err != nil {
fmt.Println(err)
return
}
defer rFile.Close()
wFile, err := os.OpenFile("go_study/copyFile.txt", os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
fmt.Println(err)
return
}
defer wFile.Close()
io.Copy(wFile, rFile)
}

13.4 目录操作
打印此前定义pkg包内的文件,文件大小单位为 字节 (byte)。
go
package main
import (
"fmt"
"os"
)
func main() {
dir, err := os.ReadDir("pkg")
if err != nil {
fmt.Println(err)
return
}
for _, entry := range dir {
info, _ := entry.Info() // 第二个参数类型是error
fmt.Println(entry.IsDir(), entry.Name(), info.Size())
}
}

14. 反射
13.1 类型判断
判断一个变量是否是 结构体、数组、map......
还有一个Java/C#中也曾见过的UnsafePointer(非安全指针)

go
package main
import (
"fmt"
"reflect"
)
func getType(obj any) {
t := reflect.TypeOf(obj)
switch t.Kind() { // 类型判断
case reflect.String:
fmt.Println("String")
case reflect.Int:
fmt.Println("Int")
case reflect.Struct:
fmt.Println("Struct")
}
}
func main() {
getType("msg")
getType(12)
getType(struct {
Name string
}{})
}

13.2 反射操作值
13.2.1 反射获取值
go
package main
import (
"fmt"
"reflect"
)
func getValue(obj any) {
v := reflect.ValueOf(obj)
switch v.Kind() {
case reflect.String:
fmt.Println("String", v.String())
case reflect.Int:
fmt.Println("Int", v.Int())
case reflect.Struct:
fmt.Println("Struct")
}
}
func main() {
getValue("msg")
getValue(12)
getValue(struct {
Name string
}{})
}

13.2.2 反射修改值
go
package main
import (
"fmt"
"reflect"
)
func setValue(obj any, value any) {
v1 := reflect.ValueOf(obj)
v2 := reflect.ValueOf(value)
if v1.Elem().Kind() != v2.Kind() { // Elem=element 从传入引用中获取元素值
return
}
switch v1.Elem().Kind() {
case reflect.String:
v1.Elem().SetString(value.(string))
case reflect.Int:
v1.Elem().SetInt(v2.Int())
}
}
func main() {
var name = "你好"
var age = 12
setValue(&name, "msg") // 传入引用
setValue(&age, 16)
fmt.Println(name)
fmt.Println(age)
}

13.3 反射操作结构体
13.3.1 反射获取值
读取JSON标签类型、值及Tag标签
go
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string `json:"name"`
Age int
Gender bool
}
func ParseJson(obj any) {
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
for i := 0; i < v.NumField(); i++ {
tf := t.Field(i)
jsonTag := tf.Tag.Get("json")
if jsonTag == "" {
jsonTag = tf.Name
}
fmt.Printf("%s, %#v\n", tf.Name, jsonTag)
fmt.Println(v.Field(i))
}
}
func main() {
s := Student{Name: "学生A", Age: 24, Gender: true}
ParseJson(s)
}

13.3.2 反射修改值
go
package main
import (
"fmt"
"reflect"
"strings"
)
type User struct {
Name1 string `big:"-"`
Name2 string
}
func setStruct(obj any) {
v := reflect.ValueOf(obj).Elem()
t := reflect.TypeOf(obj).Elem()
for i := 0; i < v.NumField(); i++ {
value := v.Field(i)
big := t.Field(i).Tag.Get("big")
if big == "" {
continue
}
value.SetString(strings.ToUpper(value.String()))
}
}
func main() {
u := User{Name1: "User B", Name2: "User B"}
setStruct(&u)
fmt.Println(u)
}

13.3.3 调用结构体方法
go
package main
import (
"fmt"
"reflect"
)
type Teacher struct {
Name1 string `big:"-"`
Name2 string
}
func (Teacher) Call(param string) {
fmt.Println("方法被调用:", param)
}
func Call(obj any) {
v := reflect.ValueOf(obj).Elem()
t := reflect.TypeOf(obj).Elem()
for i := 0; i < v.NumMethod(); i++ {
m := t.Method(i)
// 反射拿不到首字母小写的方法
fmt.Println(m.Name)
if m.Name != "Call" {
continue
}
method := v.Method(i)
method.Call([]reflect.Value{
reflect.ValueOf("参数在此"),
})
}
}
func main() {
teacher := Teacher{}
Call(&teacher)
}

13.4 反射应用
利用反射实现一个迷你ORM组件。
go
package main
import (
"errors"
"fmt"
"reflect"
"strings"
)
type ClassInSchool struct {
Name string `my-orm-tag:"name"`
Id string `my-orm-tag:"id"`
}
func Find(obj any, query ...any) (sql string, err error) {
// obj 必须是结构体
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Struct {
err = errors.New("obj必须得是结构体")
return
}
var where string
// 验证条件(问号与条件个数是否匹配)
if len(query) > 0 {
q := query[0]
// 验证第一个参数类型,必须是字符串
// string类型检测可以用:1.断言 2.反射
qs, ok := q.(string)
if !ok {
err = errors.New("查询的第一个参数必须是字符串")
return
}
// 计算问号个数
if strings.Count(qs, "?")+1 != len(query) {
err = errors.New("查询参数个数不匹配")
return
}
// 拼接SQL语句中where查询条件
for _, a := range query[1:] {
switch s := a.(type) {
// 每走一轮少一个问号
case string:
qs = strings.Replace(qs, "?", fmt.Sprintf("%s", s), 1)
case int:
qs = strings.Replace(qs, "?", fmt.Sprintf("%d", s), 1)
}
}
where = "where " + qs
}
// 拼接有my-orm-tag字段
var columns []string
for i := 0; i < t.NumField(); i++ {
orm := t.Field(i).Tag.Get("my-orm-tag")
if orm == "" {
continue
}
columns = append(columns, orm)
}
// 算表名,下划线
name := strings.ToLower(t.Name()) + "s"
sql = fmt.Sprintf("select %s from %s %s;",
strings.Join(columns, ","),
name,
where,
)
return
}
func main() {
sql, err := Find(ClassInSchool{}, "name = ?", "'三年一班'")
fmt.Println(sql, err)
// Select name, id from class_in_school where name = '三年一班';
sql, err = Find(ClassInSchool{}, "id = ? and name = ?", 1, "'三年一班'")
fmt.Println(sql, err)
// Select name, id from class_in_school where age = 1 and name = '三年一班';
sql, err = Find(ClassInSchool{})
fmt.Println(sql, err)
// Select name, id from class_in_school;
}

15. 单元测试
15.1 整体测试
Go语言中自带轻量级测试框架testing及命令go test实现单元测试与性能测试。
Go语言推荐测试代码与源代码放在一起,要求测试文件以被测文件名_test.go格式结尾,测试用例函数以Test被测函数名格式开头。
创建一个目录,并在其中写两个go文件
calc.go:方法首字母大写 (public)
go
package main
func Add(n1, n2 int) int {
return n1 + n2
}
calc_test.go
go
package main
import "testing"
func TestAdd(t *testing.T) {
res := Add(1, -1)
if res != 0 {
t.Errorf("测试失败")
return
}
t.Logf("测试通过")
}
使用GoLand测试

出现类似Java IDEA 中 Junit 的标识

使用命令测试,-v 可显示每个用例结果,-run 可指定测试函数

单元测试提供的日志方法
| 方法 | 备注 |
|---|---|
| Log | 打印日志,结束测试 |
| Logf | 格式化打印 |
| Error | 打印错误日志,结束测试 |
| Errorf | 格式化打印 |
| Fatal | 打印致命日志,结束测试 |
| Fatalf | 格式化打印 |
15.2 子测试
如果需要测试函数不同用例。可使用子测试,此时Fatal不终止程序。
go
package main
import "testing"
func TestAdd(t *testing.T) {
t.Run("add1", func(t *testing.T) {
res := Add(1, -1)
if res != 0 {
t.Errorf("测试失败")
return
}
t.Logf("测试通过")
})
t.Run("add2", func(t *testing.T) {
res := Add(2, -1)
if res != 1 {
t.Errorf("测试失败")
return
}
t.Logf("测试通过")
})
}
此时便可进行子测试


15.3 便捷子测试
go
package main
import "testing"
func TestAdd(t *testing.T) {
cases := []struct {
Name string
A, B, Expected int
}{
{"a1", 2, 3, 5},
{"a2", 2, -3, -1},
{"a3", 2, 0, 2},
}
for _, s := range cases {
t.Run(s.Name, func(t *testing.T) {
res := Add(s.A, s.B)
if res != s.Expected {
t.Errorf("测试失败")
return
}
t.Logf("测试通过")
})
}
}


Goland可快速定位到错误用例,方便进行后续调整。

也可以利用读写将这些用力写入JSON/Excel/csv文件中。
15.3 TestMain函数
TestMain函数测试入口,用于实现测试流程生命周期
go
package main
import (
"fmt"
"os"
"testing"
)
func TestAdd2(t *testing.T) {
fmt.Println("测试时")
}
func setup() {
fmt.Println("测试前")
}
func teardown() {
fmt.Println("测试后")
}
func TestMain(m *testing.M) {
fmt.Println("TestMain")
setup()
code := m.Run()
teardown()
os.Exit(code)
}
