Golang教程六(单元测试,反射,网络编程,部署)

时间:2024-04-18 11:51:43

目录

一、单元测试

单元测试 

 子测试

 TestMain

二、反射

类型判断

通过反射获取值

通过反射修改值

结构体反射

利用tag修改结构体的某些值

调用结构体方法

orm的一个小案例

对反射的一些建议

三、网络编程

socket编程

websocket编程

四、部署

打包命令

交叉编译


一、单元测试

Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言的测试框架相似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决:

  1. 确保每个函数是可运行,并且运行结果是正确的
  2. 确保写出来的代码性能是好的
  3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾

注意点:

  1. 测试用例文件名必须以_test.go结尾
  2. 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名 

单元测试 

例如我现在有两个用于计算的文件,叫calc.go

package main

func Add(a int, b int) int {
    return a + b
}

func Mul(a int, b int) int {
    return a * b
}

那么我的测试文件就是calc_test.go

package main

import "testing"

func TestAdd(t *testing.T) {
  if ans := Add(1, 2); ans != 3 {
    // 如果不符合预期,那就是测试不通过
    t.Errorf("1 + 2 expected be 3, but %d got", ans)
  }

  if ans := Add(-10, -20); ans != -30 {
    t.Errorf("-10 + -20 expected be -30, but %d got", ans)
  }
}

go test // 可以运行某个包下的所有测试用例

-v 参数会显示每个用例的测试结果

-run参数可以指定测试某个函数

单元测试框架提供的日志方法

方 法 备 注 测试结果
Log 打印日志,同时结束测试 PASS
Logf 格式化打印日志,同时结束测试 PASS
Error 打印错误日志,同时结束测试 FAIL
Errorf 格式化打印错误日志,同时结束测试 FAIL
Fatal 打印致命日志,同时结束测试 FAIL
Fatalf 格式化打印致命日志,同时结束测试 FAIL

 子测试

如果需要给一个函数,调用不同的测试用例,可以使用子测试

子测试里面的Fatal,是不会终止程序的

package main

import "testing"

func TestAdd(t1 *testing.T) {
	t1.Run("add1", func(t *testing.T) {
		if ans := Add(1, 2); ans != 3 {
			// 如果不符合预期,那就是测试不通过
			t.Fatalf("1 + 2 expected be 3, but %d got", ans)
		}
	})
	t1.Run("add2", func(t *testing.T) {
		if ans := Add(-10, -20); ans != -30 {
			t.Fatalf("-10 + -20 expected be -30, but %d got", ans)
		}
	})

}

如果测试用例很多,还可以用一个类似表格去表示

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 _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if ans := Add(c.A, c.B); ans != c.Expected {
				t.Fatalf("%d * %d expected %d, but %d got",
					c.A, c.B, c.Expected, ans)
			}
		})
	}
}

 TestMain

它是测试的入口

我们可以在TestMain里面实现测试流程的生命周期

package main

import (
	"fmt"
	"os"
	"testing"
)

// 测试前执行
func setup() {
	fmt.Println("Before all tests")
}

// 测试后执行
func teardown() {
	fmt.Println("After all tests")
}

func Test1(t *testing.T) {
	fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
	fmt.Println("I'm test2")
}

// 必须叫这个名字  测试主入口
func TestMain(m *testing.M) {
	// 测试前执行
	setup()
	code := m.Run()
	// 测试后执行
	teardown()
	os.Exit(code)
}

二、反射

类型判断

判断一个变量是否是结构体,切片,map

package main

import (
	"fmt"
	"reflect"
)

func refType(obj any) {
	typeObj := reflect.TypeOf(obj)
	fmt.Println(typeObj, "+", typeObj.Kind())
	// 去判断具体的类型
	switch typeObj.Kind() {
	case reflect.Slice:
		fmt.Println("切片")
	case reflect.Map:
		fmt.Println("map")
	case reflect.Struct:
		fmt.Println("结构体")
	case reflect.String:
		fmt.Println("字符串")
	}
}

func main() {
	refType(struct{ Name string }{Name: "os_lee"})
	name := "os_lee"
	refType(name)
	refType([]string{"os_lee"})
}

通过反射获取值

package main

import (
	"fmt"
	"reflect"
)

func refValue(obj any) {
	value := reflect.ValueOf(obj)
	fmt.Println(value, "+", value.Type())
	switch value.Kind() {
	case reflect.Int:
		fmt.Println("Int=", value.Int())
	case reflect.Struct:
		fmt.Println("Interface=", value.Interface())
	case reflect.String:
		fmt.Println("String=", value.String())
	}
}

func main() {
	refValue(struct{ Name string }{Name: "os_lee"})
	name := "os_lee"
	refValue(name)
	refValue([]string{"os_lee"})
}

通过反射修改值

注意,如果需要通过反射修改值,必须要传指针,在反射中使用Elem取指针对应的值

结构体反射

读取json标签对应的值,如果没有就用属性的名称

这个示例很简单,没有处理-和omitempty的情况

package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name string
	Age  int `json:"age"`
}

func main() {
	s := Student{
		Name: "os_lee",
		Age:  24,
	}
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		jsonField := field.Tag.Get("json")
		if jsonField == "" {
			// 说明json的tag是空的
			jsonField = field.Name
		}
		fmt.Printf("Name: %s, type: %s, json: %s, value: %v\n", field.Name, field.Type, jsonField, v.Field(i))
	}
}

利用tag修改结构体的某些值

例如,结构体tag中有big的标签,就将值大写

package main

import (
	"fmt"
	"reflect"
	"strings"
)

type Student struct {
	Name string `big:"name"`
	Addr string
}

func main() {
	s := Student{
		Name: "os",
		Addr: "bj",
	}
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(&s).Elem()

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		bigField := field.Tag.Get("big")
		// 判断类型是不是字符串
		if field.Type.Kind() != reflect.String {
			continue
		}
		if bigField == "" {
			continue
		}
		// 修改值
		valueFiled := v.Field(i)
		valueFiled.SetString(strings.ToTitle(valueFiled.String()))
	}
	fmt.Println(s)
}

调用结构体方法

如果结构体有call这个名字的方法,就执行它

package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name string
	Age  int
}

func (Student) Look(name string) {
	fmt.Println("look name:", name)
}

func (Student) See(name string) {
	fmt.Println("see name:", name)
}

func main() {
	s := Student{
		Name: "os",
		Age:  21,
	}
	t := reflect.TypeOf(s)
	v := reflect.ValueOf(s)

	for i := 0; i < t.NumMethod(); i++ {
		methodType := t.Method(i)
		fmt.Println(methodType.Name, methodType.Type)
		if methodType.Name != "See" {
			continue
		}
		methodValue := v.Method(i)
		methodValue.Call([]reflect.Value{
			reflect.ValueOf("lee"), // 注意这里的类型
		})
	}
}

orm的一个小案例

package main

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
)

type Student struct {
	Name string `oslee-orm:"name"`
	Age  int    `oslee-orm:"age"`
}

type UserInfo struct {
	Id   int    `oslee-orm:"id"`
	Name string `oslee-orm:"name"`
	Age  int    `oslee-orm:"age"`
}

// sql, err := Find(Student{}, "name = ? and age = ?", "os_lee", 18)
func Find(obj any, query ...any) (sql string, err error) {
	// Find(Student, "name = ?", "os")
	// 希望能够生成 select name, age from  where  name = 'os'
	t := reflect.TypeOf(obj)
	//v := reflect.ValueOf(obj)
	// 首先得是结构体对吧
	if t.Kind() != reflect.Struct {
		err = errors.New("非结构体")
		return
	}
	// 拿全部字段

	// 拼接条件
	// 第二个参数,中的问号,就决定后面还能接多少参数
	var where string
	if len(query) > 0 {
		// 有第二个参数,校验第二个参数中的?个数,是不是和后面的个数一样
		q := query[0] // 理论上还要校验第二个参数的类型
		if strings.Count(q.(string), "?")+1 != len(query) {
			err = errors.New("参数个数不对")
			return
		}
		// 拼接where语句
		// 将?号带入后面的参数
		for _, a := range query[1:] {
			// 替换q
			// 这里要判断a的类型
			at := reflect.TypeOf(a)
			switch at.Kind() {
			case reflect.Int:
				q = strings.Replace(q.(string), "?", fmt.Sprintf("%d", a.(int)), 1)
			case reflect.String:
				q = strings.Replace(q.(string), "?", fmt.Sprintf("'%s'", a.(string)), 1)
			}
		}
		where += "where " + q.(string)
	}
	// 如果没有第二个参数,就是查全部

	// 拼接select
	// 拿所有字段,取oslee-orm对应的值
	var columns []string
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		f := field.Tag.Get("oslee-orm")
		// 不考虑是空的情况
		columns = append(columns, f)
	}

	// 结构体的小写名字+s做表名
	name := strings.ToLower(t.Name()) + "s"

	// 拼接最后的sql
	sql = fmt.Sprintf("select %s from %s %s", strings.Join(columns, ","), name, where)
	return
}

func main() {
	sql, err := Find(Student{}, "name = ? and age = ?", "os_lee", 18)
	fmt.Println(sql, err) // select name,age from students where name = 'os_lee' and age = 18
	sql, err = Find(UserInfo{}, "id = ?", 1)
	fmt.Println(sql, err) // select id,name,age from userinfos where id = 1
}

对反射的一些建议

如果是写一下框架,偏底层工具类的操作

不用反射确实不太好写,但是如果是在业务上,大量使用反射就不太合适了

因为反射的性能没有正常代码高,会慢个一到两个数量级

使用反射可读性也不太好,并且也不能在编译期间发生错误

三、网络编程

socket编程

参考:5.网络编程-socker(golang版)-****博客

websocket编程

参考:4.网络编程-websocket(golang)-****博客

四、部署

go项目的部署特别简单,编写完成之后,只需要执行go build即可打包为可执行文件
注意,这个操作是不同平台不一样的
windows下打包就是exe文件,linux下打包就是二进制文件

打包命令

go build

打当前目录下的main包,注意,只能有一个main函数的包

go build xxx.go

打当前目录下,xxx.go的包,这个包必须得是一个main包,不然没有效果

go build -o main.exe xxx.go

强制对输出的文件进行重命名

-o参数必须得在文件的前面

交叉编译

什么是交叉编译呢,就是在windows上,我开发的go程序,我也能打包为linux上的可执行程序

例如在windows平台,打linux的包

注意,执行set这个命令,一定得要是在cmd的命令行下,powershell是无效的 

set CGO_ENABLED=0  
set GOOS=linux  
set GOARCH=amd64  
go build -o main main.go

 CGO_ENABLED : CGO 表示 golang 中的工具,CGO_ENABLED=0 表示 CGO 禁用,交叉编译中不能使用 CGO GOOS : 环境变量用于指定目标操作系统,mac 对应 darwin,linux 对应 linux,windows 对应 windows ,还有其它的 freebsd、android 等

GOARCH:环境变量用于指定处理器的类型,386 也称 x86 对应 32位操作系统、amd64 也称 x64 对应 64 位操作系统,arm 这种架构一般用于嵌入式开发。比如 Android , iOS , Win mobile 等

为了方便呢,可以在项目的根目录下写一个bat文件

这样就能快速构建了

然后放到linux服务器下,设置文件权限就可以直接运行了

chmod +x main
./main

再次注意啊,以后打包web项目的时候,配置文件和静态文件等这些非go程序,是要一起复制到目标服务器里面的

参考:Go 学习笔记(37)— 标准命令行工具(go build 跨平台编译、交叉编译、go clean、go run、go fmt、go install、go get、go vet)-****博客