Go,Gin学习
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
- 传递指针(所以不需要传递 map 的引用,直接传递就行)
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
}
}
并发
- 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 的协程
- 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
}
- 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)
}()
}
- 原子操 作
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
}
- 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 结构
- 推荐
- 关注和取消关注
- Get
-
ReviewReaction 点赞、踩
-
ReviewReply 回复
-
Review
- CRUD + 推荐, 同 course
-
Teacher
-
User