Skip to main content

Go,Gin学习

· 20 min read
ayanami

Go & Gin & Gorm 学习

Go

Learn go by test

基本语法

package:类似namespace或者module

循环

func Repeat(character string) string {
var repeat string
<!--truncate--> for i:=1;i<=5;i++{
repeat = repeat + character
}
return repeat
}

其中:=是带初始值定义的语法糖,var name type是不带初始值的定义;go 的循环只有 for

测试示例

import (
"testing"
)
func TestRepeat(t *testing.T){
got:=Repeat("a")
expect:="aaaaa"
if got!=expect{
t.Errorf("got %q, expect %q", got, expect)
}
}

使用

go test

运行测试,如果有定义 main 函数(在package main之中),则可以直接go run xx.go

go 包内函数和成员变量的可见性是通过大小写区分的,大写对包外可见,小写不可见

test 函数需要写成TestXXX的形式,同时文件名也要写成whatfiles_test

基准测试 benchmark,BenchmarkXXX函数名

strings 库和相关函数

range 迭代

func Sum(arr []int) int {
sum := 0
// index, element
for _, ele := range arr{
sum += ele
}
return sum
}
go test -cover # 测试测试覆盖率

变长参数和 append

func SumAll(arrs ...[]int) []int{
ans:=[]int{}
for _, arr := range arrs{
ans = append(ans, Sum(arr))
}
return ans
}

函数指针(函数可以直接给变量)

	checkSums := func(t testing.TB, got, want []int) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}

集合切片:go 支持类似 python 的切片

结构和方法(OOP)

import "math"
type Rectangle struct {
width float32
height float32
}
type Circle struct {
radius float32
center_x float32
center_y float32
}
func (r Rectangle) Area() float32 {
return r.width * r.height
}
func (c Circle) Area() float32 {
return math.Pi * c.radius * c.radius
}

go 不允许函数重载,类完全和结构体等价,类内函数(方法)如上定义

结构体可以嵌套,并且支持折叠调用(例如 A.B.ccc, ccc 是 B 的属性,B 是 A 的属性,可以直接 A.ccc)

go 有 interface

type Shape interface{
Area() float32
}

测试也方便

func TestArea(t *testing.T){
checkArea := func(t *testing.T, shape Shape, expect float32){
diff := shape.Area() - expect
if math.Abs(float64(diff)) > 1e-6{
t.Errorf("expect %v, got %v, diff %v", expect, shape.Area(), diff)
}
}
t.Run("rectangles", func(t *testing.T) {
rectangle := Rectangle{12, 6}
checkArea(t, rectangle, 72.0)
})

t.Run("circles", func(t *testing.T) {
circle := Circle{10, 0, 0}
checkArea(t, circle, 314.1592653589793)
})
}

go 之中接口解析是隐式的, 不需要显式 implements xxx(interface),只需要确实有这个方法,就能成功编译

Best Practice:表结构测试, 使用t.Run和测试表和#%v(打印结构)来获得清晰的测试

import (
"math"
"testing"
)
func TestArea(t *testing.T){
checkArea := func(t *testing.T, name string, shape Shape, expect float32){
t.Run(name, func(t *testing.T){
diff := shape.Area() - expect
if math.Abs(float64(diff)) > 1e-6{
t.Errorf("in %#v, expect %v, got %v, diff %v", shape, expect, shape.Area(), diff)
}
})
}
testTable:= []struct{
name string
shape Shape
expect float32
}{
{name: "Rectangle",shape: Rectangle{width: 10, height: 10}, expect: 100},
{name:"Circle", shape: Circle{radius: 10}, expect: 314.1592653589793},
}

for _, test :=range testTable{
checkArea(t, test.name, test.shape, test.expect)
}
}

指针和错误

go 的函数默认都是值传递,而指针又由于语法糖表现得和引用(的语法)差不多

func (w *Wallet) Withdraw(amount Bitcoin) {
w.balance -= amount
} // 这是this->balance -= amount
func (w Wallet) Withdraw(amount Bitcoin) {
w.balance -= amount
} // 这是w = (*this), w.balance -= amount

go 用返回值来表示一个函数可能抛出错误(其中返回值的类型为 error)

如果这个 error 是 nil(null 的变体),那就是正常,否则是错误,例如

func (w *Wallet) Withdraw(amount Bitcoin) error {
if amount > w.balance {
return errors.New("oh no")
}

w.balance -= amount
return nil
}

检测一个类是否实现了接口的全部方法:

var _ Interface = (*Class)(nil) // 如果Class类的指针不能强制转换成接口,就没有全部实现

空接口:可以表示任何类型

func main(){
m := make(map[string]interface{})
m["name"]="Tom"
m["age"]=18
m["parents"]=Persons{A, B}
}

map map[key]value

  1. 传递指针(所以不需要传递 map 的引用,直接传递就行)
  2. var m map[string]string后,m 是 nil,这不好。这不是空 map
var dictionary = map[string]string{}
// OR
var dictionary = make(map[string]string)

泛型

type List[T any] struct {
head, tail *element[T]
}
type element[T any] struct {
next *element[T]
val T
}
func (lst *List[T]) Push(v T) {
if lst.tail == nil {
lst.head = &element[T]{val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element[T]{val: v}
lst.tail = lst.tail.next
}
}

并发

  1. goroutine
package main

import (
"fmt"
"sync"
"time"
)

func ConcurrentDownload(url string) {
var wg sync.WaitGroup
download := func (name string) {
fmt.Printf("Start Downloading %v.\n", name)
time.Sleep(time.Second) // simulate
fmt.Printf("%v finished.\n", name)
wg.Done()
}
for i:=0;i<10;i++{
wg.Add(1)
go download("File"+fmt.Sprint(i))
}
wg.Wait()
}

func main(){
ConcurrentDownload("http://example.com")
}

其中,

  • sync.WaitGroup用于等待所有协程结束(其实是一个计数器,在计数为 0 的时候 Wait()就不堵塞了,Add()加一,Done()减一)
  • go <dosomething>起一个 dosomething 的协程
  1. channel

并发协程的通信管道

package main
import (
"fmt"
)
func PrintFib(n int) {
fib_chan := make(chan int)
go func(){
x := 0
y := 1
for i:=0;i<n;i++{
fib_chan <- x
x, y = y, x + y
}
close(fib_chan)
}()

for num := range fib_chan{
fmt.Printf("%d ", num)
fmt.Println()
}
}

func main(){
PrintFib(10)
}

其中,

  • make 是一个类似 malloc 的东西, 但他可以自动 gc,并且可以分配 channel 管道
  • <-是一个运算符, 对 chan 使用,代表把右边的丢进 chan
  • 用 make 创建 chan,可以有第二个参数,代表缓冲区大小
    • 如果没有,无缓冲,只有当接受和发送端都就绪才会开始发送
    • 如果有,size 为 n,意味着有一个 n 个元素大小的缓冲区,发送端可以提前发送,缓冲区满之前不需要等待接收端
  • chan 支持 range 语法,range 会沿着管道迭代直至关闭——管道只能被发送方关闭,接收方关闭管道后继续发送会 panic

用 channel 也可以进行这样的同步:

package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")

done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
<-done // 在这里起到了类似wait()或者join()的效果
}

可以指定一个 channel 只可以接受/发送

func pong(pings <-chan string, pongs chan<- string) {
// pings只能发送,pongs只能接受
msg := <-pings
pongs <- msg
}
  1. select

可以同时等待多个 channel 上的消息

package main
import (
"fmt"
"time"
)
func main() {

c1 := make(chan string)
c2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()

for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}

select 实现超时——一个协程干活,一个协程 Sleep timeout 秒,然后 select 两个协程

select 实现非堵塞管道——一个 default case,不接收东西

协程理解:

  • 轻量级线程,轻量级体现在是在用户态的堆上去模拟栈结构,在代码之中自己调度,而不是通过 syscall 陷入内核调度——其中非抢占式的实现(例如 python 的 asyncio),通过 yield 等在阻塞时主动让出;抢占式的实现可以直接被调度器调度(例如 goroutine)
  • 也因为是在用户态的调度,协程实际上没有并行的功能,只是提供了一个非阻塞的执行流,多个协程在同一个线程上执行,也只占据 cpu 的一个核心
  • 协程的用处在于,以一个轻量级的方式去完成堵塞的功能(例如 IO, network),执行两个 IO 请求,cpu 工作 1ms,IO 工作 1s,如果是两个线程,cpu 的 1ms 可以并行,但受限于线程创建回收调度开销和系统最大线程资源限制,实际损耗的时间很可能大于 ms 级别。而协程在 cpu 上需要依次运行两个 1ms,但是同样不需要被 IO 堵塞
  • 具体的实现方式而言,分为有栈协程和无栈协程,有栈协程有自己的一段模拟的栈空间,在切换时保存寄存器到栈里面;无栈协程使用状态机实现,在切换时记录状态机的状态。有栈协程开销更大,但表达力更强
  • ts 里面的 async/await 就是协程的包装,await 就是记录状态的点,程序向下执行到 await 处暂停协程,等待 await 事件完成

timer&ticker 内置计时器, 可用于限流等

defer 延迟做某件事,常见于模拟一种 RAII,无论是何种方式退出都能正常执行

func writeToFile(filename string, data []byte) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close() // 确保文件在函数结束时被关闭,defer后的调用会在defer所在的函数结束后执行

_, err = file.Write(data)
return err
}
 	var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker(i)
}()
}
  1. 原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var ops atomic.Uint64
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
ops.Add(1)
}
wg.Done()
}()
}
wg.Wait()

fmt.Println("ops:", ops.Load()) //50000
}
  1. mutex 锁
type Container struct {
mu sync.Mutex
counters int
}
func (c *Container) inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters++
}

Useful API

Sort 排序(slices) 第二个参数是谓词

自定义排序之中的内置类型的比较用cmp.Compare()

例如

PersonCmp := func(a *Person, b *Person){
return cmp.Compare(a.name, b.name)
}
slices.SortFunc(Persons, PersonCmp)
slices.Sort(ints)
slices.isSorted(ints)

go 有内置panic(msg string)recover(在 defer 内部,catch panic)

// recover 的返回值是调用 panic 时引发的错误。
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
mayPanic()

字符串相关函数

package main
import (
"fmt"
s "strings"
)
var p = fmt.Println
func main() {
p("Contains: ", s.Contains("test", "es"))
p("Count: ", s.Count("test", "t"))
p("HasPrefix: ", s.HasPrefix("test", "te"))
p("HasSuffix: ", s.HasSuffix("test", "st"))
p("Index: ", s.Index("test", "e"))
p("Join: ", s.Join([]string{"a", "b"}, "-"))
p("Repeat: ", s.Repeat("a", 5))
p("Replace: ", s.Replace("foo", "o", "0", -1))
p("Replace: ", s.Replace("foo", "o", "0", 1))
p("Split: ", s.Split("a-b-c-d-e", "-"))
p("ToLower: ", s.ToLower("TEST"))
p("ToUpper: ", s.ToUpper("test"))
}

正则:regexp

JSON 包,rand 包

parse 数字,

package main

import (
"fmt"
"strconv"
)

func main() {
f, _ := strconv.ParseFloat("1.234", 64)
i, _ := strconv.ParseInt("123", 0, 64) // 0:从字符串推断基数, 64:int64
d, _ := strconv.ParseInt("0x1c8", 0, 64)
u, _ := strconv.ParseUint("789", 0, 64)
k, _ := strconv.Atoi("135")
_, e := strconv.Atoi("wat")
}

文件 IO,完全类似 c

ReadFile 读取整个文件,Read 读取几个字节(用make([]byte, size)装),Seek 有三种方式 io.SeekStart, io.SeekCurrent, io.SeekEnd,bufio 提供带缓冲 IO

Write, WriteFile, Create, Flush, Sync, WriteString(in bufio)同理

删除文件 os.Remove

文件路径path/filepath包,filepath.Join 连接路径 Ext 得到拓展名, Dir 得到文件目录,Base 得到文件名

文件夹 os.Mkdir, os.RemoveAll (等效于rm -rf), os.Chdir, os.ReadDir(得到文件的集合)

WalkDir 递归遍历

err = filepath.WalkDir("subdir", visit)
func visit(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(" ", path, d.IsDir())
return nil
}

//go:embed 在编译出的二进制文件之中包含

//go:embed folder/single_file.txt
var fileString string // 将folder/single_file.txt的内容放到fileString里面,并在编译时带上该txt

os.Args 命令行参数

os.Getenv / Setenv 环境变量


Gin

start

r 指定一个 wsgi 应用实例

GET 路由响应

Run() 可以带一个参数,例如Run(":9090")这里使用默认 8080

package main

import (
"github.com/gin-gonic/gin"
)

func main(){
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

路由,GET,POST,PUT,DELETE,...

动态路由,

r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
}) // 匹配/user/ayanami


Query

// 匹配users?name=xxx&role=xxx,role可选
r.GET("/users", func(c *gin.Context) {
name := c.Query("name")
role := c.DefaultQuery("role", "admin")
c.String(http.StatusOK, "%s is a %s", name, role)
})

POST

r.POST("/form", func(c *gin.Context) {
username := c.PostForm("username")
password := c.DefaultPostForm("password", "000000") // 可设置默认值

c.JSON(http.StatusOK, gin.H{
"username": username,
"password": password,
})
})

热更新:使用 air (https://github.com/air-verse/air/blob/master/README-zh_cn.md)

重定向

r.GET("/redirect", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/index")
})

上传文件

r.POST("/upload1", func(c *gin.Context) {
file, _ := c.FormFile("file")
// c.SaveUploadedFile(file, dst)
c.String(http.StatusOK, "%s uploaded!", file.Filename)
})

ShouldBindJson, ShouldBindQuery, ShouldBindUri 将传入的 json, xml, uri 等尝试解析到某个结构体, 解析失败返回 err

	var request dto.CourseDetailRequest
if err := c.ShouldBindUri(&request); err != nil {
c.JSON(http.StatusNotFound, dto.BaseResponse{Message: "参数错误"})
return
}
course, err := service.GetCourseDetail(c, request.CourseID) // 成功解析就可以用成员变量了

type CourseDetailRequest struct {
CourseID int64 `uri:"courseID" binding:"required"`
}

指定 binding:"required"会拒绝空参数,自动校验

还可以在 binding 里面指定自定义验证器,具体看官方文档,参数比较多,完成表单验证功能,通过反射

文件上传和返回: FormFile 和 SaveUploadFile, 还有 MaxMultipartMemory

func main() {
router := gin.Default()
// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单文件, 这里的file实际上是FileHeader的类型,可以有file.Filename .Size得到一些基础信息
file, _ := c.FormFile("file")
log.Println(file.Filename)

dst := "./" + file.Filename
// 上传文件至指定的完整文件路径
c.SaveUploadedFile(file, dst)

c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}

中间件和分组路由

分组:管理路由

v1 := r.Group("/v1").Use(publicHandler)
{
v1.GET("/posts", defaultHandler1)
v1.GET("/series", defaultHandler2)
}

中间件:在请求到达路由的前后进行的一系列操作(例如 jwt 鉴权) Use()api

就是一个返回 c *gin.Context的 func

Context:某种键值对

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()

// 设置 example 变量
c.Set("example", "12345")

// 请求前,Next()前

c.Next() // 传递给下一个中间件

// 请求后,回来的时候
latency := time.Since(t)
log.Print(latency)

// 获取发送的 status
status := c.Writer.Status()
log.Println(status)
}
}

实际上是洋葱模式

中间件嵌套 m1 before -> m2 before -> core -> m2 after -> m1 after

还有一个 api 是 c.Abort() 例如用户验证失败时,调用它阻止继续向下一层中间件传递

上一层 Set 了之后,下一层就可以从 Context 里面 Get

日志:

func main() {
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
gin.DisableConsoleColor()

// 记录到文件。
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)

// 如果需要同时将日志写入文件和控制台,请使用以下代码。
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})

router.Run(":8080")
}

自定义日志:看文档

灵活度不够、日志拆分 go-logging, logrus(recommended)

成品日志包

gorm

这个更是依托,全看文档就行,里面一堆指针乱飞和神奇语义和类似 Any 的字符串反射都是依托

建议始终开文档开发

Open 链接,AutoMigrate 建表

增删改 Create Delete Update、Updates(Update key value, Updates map/struct) (Save)

查 First Find 根据有无 ID(主键)决定查全表还是查一个具体的,如果查全表传一个空对象的指针就行,First 单个,Find 查切片

条件 Where Or 填简单 sql

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

Delete 默认软删除(如果模型有 DeleteAt 字段,例如使用 gorm.Modal)

用 Unscoped().Delete()硬删除

结构体 tag + 一对一、一对多、多对多:看文档

有 django 一脉相承的手动 Preload()

JWT: jwt-go 文档


实战:jcourse_go

main.go

  • Init() godotenv 从.env文件之中读环境变量
  • 链接 Redis, DB, OpenAI api
  • registerRouter, 启动 gin Engine

router.go

  • InitSession() 从环境变量 SESSION_SECRET 获取密钥后 tcp 链接 Redis,集成进入 gin (Sessions API 指定存的地方 NewRedisStore 就行)

  • 路由组:

    • authGroup 登陆相关,不需要 auth
    • needAuthGroup 其他非 Admin(teacher, course, review, user) 过中间件 RequireAuth() 判断 Session()里面有没有 Use
    func RequireAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
    session := sessions.Default(c)
    user := session.Get(constant.SessionUserAuthKey)
    if user == nil {
    c.JSON(http.StatusUnauthorized, dto.BaseResponse{Message: "未授权的请求"})
    c.Abort()
    }
    c.Next()
    }
    }

    • adminGroup 过中间件 RequireAdmin() 就是再加了一个判断 User 的 Role

handlers(某种意义上的 Controllers in java)

  • auth: 很重复的东西

  • course:

    • Get
      • 从设置里面拿到 page 和 pageSize 参数构造 request
      • 将 Query 绑定到 request(读取参数)
      • 做一个从 request 的,,,的 json 形式转换为数组再构造 filter 结构
      • 传给 service 的 api
      • 返回结果过 DTO,Total, Data, Page, PageSize 统一 BasePaginateResponse 结构
    • 推荐
    • 关注和取消关注
  • ReviewReaction 点赞、踩

  • ReviewReply 回复

  • Review

    • CRUD + 推荐, 同 course
  • Teacher

  • User

Loading Comments...