0


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

一、单元测试

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打印日志,同时结束测试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博客


本文转载自: https://blog.csdn.net/qq_22321199/article/details/137737770
版权归原作者 os_lee 所有, 如有侵权,请联系我们删除。

“Golang教程六(单元测试,反射,网络编程,部署)”的评论:

还没有评论