Files
rikako-note/Golang/Golang Document.md
2025-01-09 01:43:35 +08:00

313 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 textfuzz无法预知传递的测试参数也无法预测参数所对应的结果。
> 在编写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数量通常会因发现新的代码路径快速增加然后会随着时间的推移逐渐减少