377 lines
12 KiB
Markdown
377 lines
12 KiB
Markdown
# Golang
|
||
## Get Started
|
||
### Enable dependency tracking
|
||
当代码对其他module中包含的package进行了import时,在自己的module中来管理依赖。
|
||
|
||
自己的module通过`go.mod`文件来定义,`go.mod`文件中会track项目所需要的依赖。
|
||
|
||
#### go mod init
|
||
`go mod init <module-name>`命令会创建一个`go.mod`文件,其中`<module-name>`会是module path。
|
||
|
||
在实际开发中,module name通常是source code被保存的repository location,例如,`uuid`module的module name为`github.com/google/uuid`。
|
||
|
||
#### go mod tidy
|
||
`go mod tidy`命令会根据import添加缺失的module并且移除未使用的module。
|
||
|
||
## multi-module workspace
|
||
示例目录结构如下所示:
|
||
- workspace
|
||
- workspace/hello
|
||
- workspace/example/hello
|
||
|
||
### go work init
|
||
在本示例中,为了创建多module的workspace,可以执行`go work init ./hello`,其会创建`go.work`文件,并将`./hello`目录下的module包含到`go.work`文件中。
|
||
|
||
`go.work`内容如下:
|
||
```
|
||
go 1.18
|
||
|
||
use ./hello
|
||
```
|
||
|
||
|
||
### go work use
|
||
通过`go work use ./example/hello`命令,会将`./example/hello`中的module加入到`go.work`文件中。
|
||
|
||
`go.work`内容如下:
|
||
```
|
||
go 1.18
|
||
|
||
use (
|
||
./hello
|
||
./example/hello
|
||
)
|
||
```
|
||
|
||
`go work use [-r] [dir]`命令行为如下:
|
||
- 如果指定目录存在,会为`dir`向`go.work`文件中添加一条use指令
|
||
- 如果指定目录不存在,会删除`go.work`文件中关于目录的use指令
|
||
|
||
## Gin框架构建restful api
|
||
在构建resultful api时,通常都会通过json格式来传递数据,首先,可定义业务实体:
|
||
```go
|
||
// album represents data about a record album.
|
||
type album struct {
|
||
ID string `json:"id"`
|
||
Title string `json:"title"`
|
||
Artist string `json:"artist"`
|
||
Price float64 `json:"price"`
|
||
}
|
||
```
|
||
|
||
### 向response中写入返回数据
|
||
可以通过调用`gin.Context`的`IndentedJSON`方法来向response中写入数据,
|
||
```go
|
||
// getAlbums responds with the list of all albums as JSON.
|
||
func getAlbums(c *gin.Context) {
|
||
c.IndentedJSON(http.StatusOK, albums)
|
||
}
|
||
```
|
||
|
||
### 解析request中的数据
|
||
可以通过`gin.Context`的`BindJSON`方法来将请求体中的数据解析到对象中。
|
||
```go
|
||
// postAlbums adds an album from JSON received in the request body.
|
||
func postAlbums(c *gin.Context) {
|
||
var newAlbum album
|
||
|
||
// Call BindJSON to bind the received JSON to
|
||
// newAlbum.
|
||
if err := c.BindJSON(&newAlbum); err != nil {
|
||
return
|
||
}
|
||
|
||
// Add the new album to the slice.
|
||
albums = append(albums, newAlbum)
|
||
c.IndentedJSON(http.StatusCreated, newAlbum)
|
||
}
|
||
```
|
||
|
||
### 将请求的endpoint注册到server中
|
||
可以将各个请求的处理handler注册到server中,并在指定端口上运行server:
|
||
```go
|
||
func main() {
|
||
router := gin.Default()
|
||
router.GET("/albums", getAlbums)
|
||
router.POST("/albums", postAlbums)
|
||
|
||
router.Run("localhost:8080")
|
||
}
|
||
```
|
||
上述示例中,分别向`/albums`路径注册了GET和POST的处理handler,并在`localhost:8080`上对服务进行监听。
|
||
|
||
## golang generic
|
||
### 不使用泛型的代码编写
|
||
如果不使用泛型,那么对于不同数值类型的求和,需要编写多个版本的代码,示例如下:
|
||
```go
|
||
// SumInts adds together the values of m.
|
||
func SumInts(m map[string]int64) int64 {
|
||
var s int64
|
||
for _, v := range m {
|
||
s += v
|
||
}
|
||
return s
|
||
}
|
||
|
||
// SumFloats adds together the values of m.
|
||
func SumFloats(m map[string]float64) float64 {
|
||
var s float64
|
||
for _, v := range m {
|
||
s += v
|
||
}
|
||
return s
|
||
}
|
||
|
||
```
|
||
上述针对int64和float64的版本,编写了两个独立的函数
|
||
|
||
### 使用泛型的代码编写
|
||
对于泛型方法的编写,其相对普通方法多了`类型参数`,在对泛型方法进行调用时,可以传递类型参数和普通参数。
|
||
|
||
对于每个`parameter type`,其都有对应的`type constraint`。每个`type constraint`都制定了在调用泛型方法时,可以对`parameter type`指定哪些类型。
|
||
|
||
`type parameter`通常都带代表一系列类型的集合,但是在编译时type parameter则是代表由调用方传递的`type argument`类型。如果type argument类型不满足type constraint的要求,那么该代码则不会被成功编译。
|
||
|
||
type parameter必须要支持`generic code`中执行的所有操作。
|
||
|
||
```go
|
||
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
|
||
// as types for map values.
|
||
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
|
||
var s V
|
||
for _, v := range m {
|
||
s += v
|
||
}
|
||
return s
|
||
}
|
||
```
|
||
#### comparable
|
||
在上述示例中,`K`的constraint为`comparable`,go中预先声明了该constraint。comparable约束代表其可以类型可以通过`==`或`!=`符号来进行比较。
|
||
|
||
golang中要求key类型为comparable。
|
||
|
||
上述示例中,V的类型约束为`float64 | int64`,`|`符号代表取两种类型的并集,实际类型可以为两种类型中的任何一种。
|
||
|
||
### 泛型方法调用
|
||
```go
|
||
// 指定类型参数
|
||
fmt.Printf("Generic Sums: %v and %v\n",
|
||
SumIntsOrFloats[string, int64](ints),
|
||
SumIntsOrFloats[string, float64](floats))
|
||
|
||
// 不指定类型参数
|
||
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
|
||
SumIntsOrFloats(ints),
|
||
SumIntsOrFloats(floats))
|
||
```
|
||
在调用golang的泛型方法时,可以省略类型参数,此时编译器会推断类型参数类型,但是对于部分场景,例如没有参数只有返回值的参数`func returnObj[V any]() V`,此时泛型类型无法被推断,只能手动指定。
|
||
|
||
### type constraint
|
||
在golang中,支持将泛型约束声明为接口,并在多个地方重用该接口。通过将约束声明为接口,可以避免复杂泛型约束的重复声明。
|
||
|
||
可以将泛型constraint声明为接口,并且允许任何类型实现该接口,将接口用作type constraint的指定,那么传递给方法的任何类型参数都要实现该接口,包含该接口中所有的方法。
|
||
|
||
代码示例如下:
|
||
```go
|
||
type Number interface {
|
||
int64 | float64
|
||
}
|
||
|
||
// SumNumbers sums the values of map m. It supports both integers
|
||
// and floats as map values.
|
||
func SumNumbers[K comparable, V Number](m map[K]V) V {
|
||
var s V
|
||
for _, v := range m {
|
||
s += v
|
||
}
|
||
return s
|
||
}
|
||
|
||
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
|
||
SumNumbers(ints),
|
||
SumNumbers(floats))
|
||
```
|
||
## Fuzzing
|
||
如下为一个反转字符串的示例:
|
||
```go
|
||
package fuzz
|
||
|
||
func Reverse(str string) (ret string, err error) {
|
||
bytes := []byte(str)
|
||
for i, j := 0, len(bytes)-1;i<j; i, j = i+1, j-1 {
|
||
bytes[i], bytes[j] = bytes[j], bytes[i]
|
||
}
|
||
ret = string(bytes)
|
||
return
|
||
}
|
||
```
|
||
### unit test编写
|
||
如果要对上述代码进行单元测试,可以手动在代码中指定测试用例,并对返回值进行断言判断。
|
||
|
||
如果想要针对golang文件创建unit test,可以按照如下步骤
|
||
- 为go文件创建对应的`_test.go`文件,例如`reverse.go`文件,其对应单元测试文件为`reverse_test.go`
|
||
- 为go文件中想要测试的方法,在`_test.go`文件中创建对应的`TestXxxx`方法,例如对`Reverse`方法,可以创建名为`TestReverse`的测试方法
|
||
|
||
在完成上述步骤并且完成测试方法逻辑的编写后,可以调用`go test`命令执行单元测试。
|
||
|
||
示例如下所示:
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
func TestReverse(t *testing.T) {
|
||
testcases := []struct {
|
||
in, want string
|
||
}{
|
||
{"Hello, world", "dlrow ,olleH"},
|
||
{" ", " "},
|
||
{"!12345", "54321!"},
|
||
}
|
||
for _, tc := range testcases {
|
||
rev := Reverse(tc.in)
|
||
if rev != tc.want {
|
||
t.Errorf("Reverse: %q, want %q", rev, tc.want)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
### fuzz test编写
|
||
在对功能进行测试时,相对于手动编码测试用例,fuzz能够自动生成用例,并可能触及到手动用例无法触及的边界用例。
|
||
|
||
但是,相较于unit text,fuzz无法预知传递的测试参数,也无法预测参数所对应的结果。
|
||
|
||
> 在编写fuzz test时,可以将unit test, fuzz test, benchmark都包含在同一个`_test.go`文件中
|
||
|
||
在编写fuzz test方法时,步骤如下
|
||
- fuzz test方法以`FuzzXxx`开头,和`TestXxx`类似
|
||
- 当调用`f.Add`时,会将参数作为seed添加
|
||
|
||
Fuzz test示例如下:
|
||
```go
|
||
func FuzzReverse(f *testing.F) {
|
||
testcases := []string{"Hello, world", " ", "!12345"}
|
||
for _, tc := range testcases {
|
||
f.Add(tc) // Use f.Add to provide a seed corpus
|
||
}
|
||
f.Fuzz(func(t *testing.T, orig string) {
|
||
rev := Reverse(orig)
|
||
doubleRev := Reverse(rev)
|
||
if orig != doubleRev {
|
||
t.Errorf("Before: %q, after: %q", orig, doubleRev)
|
||
}
|
||
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
|
||
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
|
||
}
|
||
})
|
||
}
|
||
```
|
||
#### 执行fuzz test
|
||
在编写完上述代码后,首先应该执行`go test`命令,确保作为seed被添加的cases能够通过测试
|
||
```bash
|
||
go test
|
||
```
|
||
|
||
> 如果,只想执行指定的测试方法,可以指定`-run`参数,示例如下
|
||
>
|
||
> ```bash
|
||
> go test -run=FuzzReverse
|
||
> ```
|
||
|
||
在`go test`执行通过之后,应该执行`go test -fuzz=Fuzz`命令,其会将随机产生的输入作为测试。
|
||
|
||
如果存在随机生成的用例测试不通过,会将该用例写入到seed corups文件中,`在下次go test命令被执行时,即使没有指定-fuzz选项,被写入文件中的用例也会被在此执行`。
|
||
|
||
> 当通过`go test -fuzz=Fuzz`执行fuzz test时,测试会一直进行,直到被`Ctrl + C`中断。
|
||
>
|
||
> 如果要指定fuzz test的时间,可以指定`-fuzztime`选项。
|
||
>
|
||
> 示例如下:
|
||
> ```bash
|
||
> go test -fuzz=Fuzz -fuzztime 30s
|
||
> ```
|
||
|
||
fuzz test的输出结果示例如下:
|
||
```powershell
|
||
PS D:\CodeSpace\demo\fuzz> go test -fuzz=Fuzz -fuzztime 10s
|
||
fuzz: elapsed: 0s, gathering baseline coverage: 0/56 completed
|
||
fuzz: elapsed: 0s, gathering baseline coverage: 56/56 completed, now fuzzing with 32 workers
|
||
fuzz: elapsed: 3s, execs: 3028153 (1008398/sec), new interesting: 2 (total: 58)
|
||
fuzz: elapsed: 6s, execs: 6197524 (1057429/sec), new interesting: 2 (total: 58)
|
||
fuzz: elapsed: 9s, execs: 9423882 (1075420/sec), new interesting: 2 (total: 58)
|
||
fuzz: elapsed: 10s, execs: 10482150 (926639/sec), new interesting: 2 (total: 58)
|
||
PASS
|
||
ok git.kazusa.red/asahi/fuzz-demo 10.360s
|
||
```
|
||
> #### new interesting
|
||
> `new interesting`指会扩充code coverage的用例输入,在fuzz test刚开始时,new interesting数量通常会因发现新的代码路径快速增加,然后,会随着时间的推移逐渐减少
|
||
|
||
## syntax
|
||
### iota
|
||
`iota`关键字代表连续的整数变量,`0, 1, 2`,每当`const`关键字出现时,其重置为0
|
||
|
||
其使用示例如下
|
||
```go
|
||
package main
|
||
|
||
import "fmt"
|
||
|
||
const (
|
||
ZERO = iota
|
||
ONE
|
||
)
|
||
|
||
const TWO = iota
|
||
|
||
const (
|
||
THREE = iota
|
||
FOUR
|
||
)
|
||
|
||
func main() {
|
||
fmt.Println(ZERO) // 0
|
||
fmt.Println(ONE) // 1
|
||
fmt.Println(TWO) // 0
|
||
fmt.Println(THREE) // 0
|
||
fmt.Println(FOUR) // 1
|
||
}
|
||
```
|
||
|
||
另外,const关键字也支持如下语法:
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"reflect"
|
||
)
|
||
|
||
type Shiro uint8
|
||
|
||
const (
|
||
ZERO Shiro = iota
|
||
ONE
|
||
)
|
||
|
||
const TWO Shiro = iota
|
||
|
||
const (
|
||
THREE Shiro = iota
|
||
FOUR
|
||
)
|
||
|
||
func main() {
|
||
fmt.Printf("ZERO %d, %s\n", ZERO, reflect.TypeOf(ZERO).Name())
|
||
fmt.Printf("ONE %d, %s\n", ONE, reflect.TypeOf(ONE).Name())
|
||
fmt.Printf("TWO %d, %s\n", TWO, reflect.TypeOf(TWO).Name())
|
||
fmt.Printf("THREE %d, %s\n", THREE, reflect.TypeOf(THREE).Name())
|
||
fmt.Printf("FOUR %d, %s\n", FOUR, reflect.TypeOf(FOUR).Name())
|
||
}
|
||
```
|
||
当const在`()`中声明多个常量时,如首个常量的类型被指定,则后续常量类型可省略,后续类型与首个被指定的类型保持一致。
|
||
|