【Golang基础】基础知识(下)

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)
}
相关推荐
yugi9878388 小时前
基于遗传算法优化主动悬架模糊控制的Matlab实现
开发语言·matlab
weixin_516023079 小时前
linux下fcitx5拼音的安装
linux·运维·服务器
moxiaoran57539 小时前
Go语言的错误处理
开发语言·后端·golang
hunter14509 小时前
Linux 进程与计划任务
linux·运维·服务器
yugi98783810 小时前
MATLAB的多层感知器(MLP)与极限学习机(ELM)实现
开发语言·matlab
楼田莉子10 小时前
Linux学习之磁盘与Ext系列文件
linux·运维·服务器·c语言·学习
陌上花开缓缓归以10 小时前
linux 怎么模拟系统panic重启
linux·运维·服务器
Never_Satisfied10 小时前
C#获取汉字拼音字母方法总结
开发语言·c#
zh_xuan10 小时前
kotlin 密封类
开发语言·kotlin
月白风清江有声10 小时前
vscode使用git
linux·运维·服务器