一、单元测试
Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言的测试框架相似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决:
- 确保每个函数是可运行,并且运行结果是正确的
- 确保写出来的代码性能是好的
- 单元测试能及时的发现程序设计或实现的逻辑错误,使问题暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定
Go 语言推荐测试文件和源代码文件放在一块,测试文件以
_test.go
结尾
注意点:
- 测试用例文件名必须以_test.go结尾
- 测试用例函数必须以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打印日志,同时结束测试PASSLogf格式化打印日志,同时结束测试PASSError打印错误日志,同时结束测试FAILErrorf格式化打印错误日志,同时结束测试FAILFatal打印致命日志,同时结束测试FAILFatalf格式化打印致命日志,同时结束测试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版)-CSDN博客
websocket编程
参考:4.网络编程-websocket(golang)-CSDN博客
四、部署
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)-CSDN博客
版权归原作者 os_lee 所有, 如有侵权,请联系我们删除。