go

变量声明

使用 var 关键字

var name string = "wsh"

var (
    name string
    isReady bool
    age int
)

短变量声明 := (在函数内部使用,可以自动推导变量类型)

playerName := "AlphaPlayer"

不可变变量使用 const 声明

const sercet string = "wsh"

数据类型

基础类型

  • int
  • float
  • str // 不可变
  • bool

聚合类型

  • array //长度固定,值类型

通过 []Type 声明

func main() {
    var v [1]int
    v[0] = 1
    fmt.Println(v)
    v1 := [2]string{"1","2"}
}

支持切片 ,通过 len() cap() 获取切片的长度和容量

v[low:high]
  • struct

引用类型

  • slice // 是动态数组,或者说和数组很相似,[]int,不指定长度

切片可以从现有数组中截取获得

numbers := [5]int{10, 20, 30, 40, 50}
numslice = numbers[2:4]

或者可以创建一个切片,也是创建动态数组的方式,如果向其中添加元素超出了 cap ,会重新找一块内存,把原先的数据拷贝过去,至于原先的数据,如果没有其他切片指向,就会被回收,否则仍然留存

s := make([]int, 5, 10)
arg1 _ 类型
arg2 _ 长度
arg3 _ 容量
  • map // map[string]int 键值对
// 使用 make 初始化
s := make([]int, 5)
  • channel // chan int 协程间通信
  • func // 函数
  • interface{}

循环

只有 for 本身

sum := 0
for i := 0; i < 5 ; i++ {
    sum += i
}
fmt.Println(sum)

if

不用带括号 () ,

i := 0
if i < 5 {
    i+=1
}
fmt.Println(i)

可以在条件之前带一句初始化代码 ,switch 一样。

defer

defer 修饰的语句会正常求值,但是在当前函数返回后才会执行,类似栈结构,先 defer 的后执行。当函数 panic 时也会执行。

func main() {
    defer fmt.println("1")
    defer fmt.Println("world")
    fmt.Println("hello")
}

类型断言

当把一个具体的值比如 int赋值给接口,这个值就类似被封装, 编译器只知道 data 是个接口,不知道它底层是 int 还是 string,就没办法进行 + 运算

var data interface{}
data = 100

需要

value, ok := data.(int)

接口定义了一组方法,只要某个结构体实现了这些方法,它就是这个接口类型, 函数的参数是接口类型,那么实现这个接口定义方法的结构体都能传入

type Animal interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "ww" }
type Cat struct{}
func (c Cat) Speak() string { return "mm" }
func MakeSound(animal Animal) {
    fmt.Println(animal.Speak())
}

interface{} 和 any 类型可以存储任意类型的数据.

当把一个具体的结构体赋值给一个接口变量时,go 编译器会屏蔽掉该结构体特有的字段和方法,只保留接口定义的方法,一个接口类型底层存储的是 (Type Value),Type 记录了盒子里面装的是什么类型,而 Value 指向那个具体结构体的内存地址。一般习惯把大对象传指针,避免拷贝造成的性能开销

协程

go 可以进行多线程,最大化利用 cpu, 只需要在协程前加一个 go 关键字

go task(1)
// 主进程

但是主进程执行过快,执行完之后会关闭暂且未执行完毕的协程,所以需要使用 sync.WaitGroup 等待协程执行

// 声明
var wg sync.WaitGroup
// 在每一个协程中添加 wg.Add(1) defer wg.Done() ,主进程写 wg.Wait()

协程之间要传输数据,使用 channel 传输,而不去使用共享内存。

// 初始化一个 channel 传递 t 类型数据
t_data := make(chan t)
// 传输数据
t_data <- "data"
//接受数据
value := <-ch

默认情况下,管道是非缓冲的。发送方发了数据,必须等接收方拿走,发送方才能继续往下走,主进程发送了数据没有被接收,会导致阻塞.如果 make 时设置了容量就是有缓存的,满了才阻塞发送

t_data := make(chan t,10)

Select

select 和 switch 一样,select 用于管道通信的,超时控制,非阻塞,多个数据源选择收到数据最快的管道等

func worker(quit chan bool) {
    for {
        select {
        case <-quit:
            fmt.Println("接收到退出命令")
            return // 结束函数
        default:
            fmt.Println("......")
            time.Sleep(1 * time.Second)
        }
    }
}
select {
case msg := <-ch:
    fmt.Println("收到消息:", msg)
default:
    // 如果 ch 里没数据,select 不会卡住,而是立刻跳到这里
    fmt.Println("没接受到数据,执行其他操作")
}

sync.Mutex

常用 api

Lock()
Unlock()
TryLock()

使用 Lock() 之后如果程序 panic ,会导致死锁,所以会用 defer ,保证即使非正常退出,也会解锁。

把锁和它要保护的数据放在同一个结构体里。

import "sync"

type SafeBank struct {
    Money int
    mu     sync.Mutex
}

func (s *SafeBank) cq(n int){
    s.mu.Lock()
    defer s.mu.Unlock()
    s.Money += n
}

运行前检查

// 格式化代码
go fmt ./...

// 静态检查
go vet ./...

// 编译检查
go build

go run m.go

go 的导入包是

import "projectName/folderName"

folderName.FuncName

gin 框架

go mod init projectName

go mod tidy

整理依赖,然后 go run name.go 运行,也可以 go build name.go 编译为 exe 文件。

import "github.com/gin-gonic/gin"
读取请求数据

api参数通过Context的Param方法来获取

r.GET("/string/:name",func(c *gin.Context){
    name := c.Param("name")
    fmt.Println("hello %s",name)
})

对于 get 请求,可以通过 DefaultQuery 或者 Query 去获取相应参数的值

r.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    lastname := c.Query("lastname")
    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

对于 post 请求,获取其中的 json 数据,首先要先将其绑定到一个结构体上,使用 ShouldBindJSON 方法

type Login struct {
    User string `json:"user" binding:"required"`
    Password string `json:"password" binding:"required"`
}

r.POST("/login", func(c *gin.Context) {
    var json Login
    if err := c.ShouldBindJSON(&json); err != nil {
        c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
        return
    }
    if json.User == "admin" && json.password == "admin" {
        c.JSON{http.StatusOK.gin.H{"status":"success login"}}
    }
    else {
        c.JSON{http.StatusUnauthorzied,gin.H{"status":"unauthorized"}}
    }
})

表单参数获取通过 PostForm 方法

r.POST("/form", func(c *gin.Context){
    type := c.DefaultPostForm("type","alert")
    msg := c.PostForm("msg")
})

获取请求头和客户端 ip

c.ClientIP()
c.GetHeader("xxx")
设置响应数据

有以下方法

c.JSON(200,gin.H{"msg":"success"})
c.String(200,"Hello %s",name)
c.HTML(200,"index.html",data)
c.Data(200,"image/png",bytes)
c.Redirect(301,"/path")
c.Status(200)
c.ProtoBuf(200, data)
c.File("/path/to/file")
c.SetCookie()
路由群组
v1 := r.Group("/admin"){
    v1.GET("/profile",profileHandlerFunc)
}

v2 := r.Group("/user") {
    v2.GET("/login" ,loginHandlerFunc)
}
中间件
  • 请求进来时,可以鉴权,记录开始时间,限流
  • 请求出去时,可以记录日志,计算耗时,统一错误处理,

有一些方法, c.Next() c.Abort() c.Set() c.Get()

c.Next(), 暂停当前中间件的执行,执行下一个中间件或者 Handler ,等它们执行完成后再执行 c.Next() 下面的代码,不调用会按顺序执行下一个中间件或者 Handler。return 也会导致程序以外当前中间件执行完了,直接去执行下一个中间件。

c.Abort() ,阻断调用链中的后续处理函数,(鉴权失败后调用),调用 c.Abort() 之后,当前函数的代码会跑完,所以通常后面接 return

c.Set() 存入数据,可以在其他中间件使用 c.Get() 取出。因为本质上是存储在 gin.Context 的一个 map[string]interface{} 中,Map 的 Value 是空接口 interface{},能存入any类型的数据,但是取出来的时候不知道是什么类型的数据,需要使用 .(type)

c.Set("userId", 10086)
value, exists := c.Get("userId")
phoneNum = value.(int)

使用中间件进行日志记录,没办法截取 c.Writer 响应的数据,因此使用一个中间件和一个嵌入 gin.ResponseWriter 和 buffer 的结构体,以及为这个结构体重写的 write 方法,记录响应数据,这里有一个嵌入,

type ResponseWriter struct {
    gin.ResponseWriter
    Body *bytes.Buffer
}
// 不对 gin.ResponserWriter 命名,这样可以继承这个接口的所有方法 Write Status 等
// 然后重写定义 ResponseWriter 的 Writer 方法,覆盖原有的方法

func (w *ResponseWriter) Write(bytes []byte) (int,error) {
    w.Body.Write(bytes)
    return w.ResponseWriter.Writer(bytes)
}

func (w *ResponseRecorder) WriteString(strings string) (int, error) {
    w.Body.WriteString(strings)
    return w.ResponseWriter.WriteString(strings)
}

// 中间件

func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        blw := &ResponseWriter{
            Body:           bytes.NewBufferString(""),
            ResponseWriter: c.Writer,
        }
        c.Writer = blw // 这一步完成替换
        c.Next()
        // contentType := c.Writer.Header().Get("Content-Type")
        statusCode := c.Writer.Status()
        path := c.Request.URL.Path
        log.Printf(`[+] 请求响应数据: %s
                    [+] 请求路径: %s
                    [+] 状态码: %d\n`, blw.Body.String(), path, statusCode)

    }
}

img

img

闭包

gin 框架的路由处理器的函数或者中间件的函数仅接受一个参数,*gin.Context ,如果需要传入额外的参数,会使用闭包。

编译器发现外层函数的一个变量在返回的函数中仍然要使用,将其从栈移到堆上,返回的内部函数拿着该变量的指针

func createCounter() func() {
    i := 0
    return func() {
        i++
        fmt.Println("当前的 i 值:",i)
    }
}

func main() {
    counterA := createCounter()

    counterB := createCounter()

    // 这是两个不一样的包,不会影响彼此
}
func AuthMiddle(secret string) gin.HandlerFunc {

}

数据库交互 ORM

go 中的结构体数据存储在数据库中通过 GORM 。GORM 提供了一个内置的结构体 gorm.model ,将其嵌入结构体中,数据库中的表会有四个通用列

import "gorm.io/gorm"
import "gorm.io/driver/postgres"

type User struct {
    gorm.model
    Username string `json:"user"`
    Password string `json:"password"`
}
初始化连接数据库
import "fmt"
import "log"
import "time"
import "gorm.io/driver/postgres"
import "gorm.io/gorm"
import "gorm.io/gorm/logger"

var DB *gorm.DB

func InitDB() {
    host := "127.0.0.1"
    user :=
    password :=
    dbname :=
    port :=
    sslmode := disable
    TimeZone := "Asia/Shanghai"
    dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s",
        host, user, password, dbname, port, sslmode, TimeZone)


    //pgConfig := postgres.Config{DSN:dsn}
    gormConfig := &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info)
    }

    var err error
    DB,err = gorm.Open(postgres.Open(dsn),gormConfig)
    if err != nil {
        panic("[-] 连接数据库失败:"+err.Error())
    }

    // 连接池配置
    sqlDB, _ := DB.DB()
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)
    fmt.Println("[+] 数据库连接成功")
}

docker pull 一个 postgres 镜像,手动创建一个 ctf_test 数据库,或者也可以先连接默认的库 postgresimg

docker run --name postgres \
-e POSTGRES_PASSWORD=wsh123456 \
-p 5432:5432 \
-d postgres:15-alpine
//db.go
package database

import (
    "log"

    "golearn/config"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

var DB *gorm.DB

func InitDB() *gorm.DB {
    cfg := config.GetDBConfig()
    dsn := cfg.ToDSN()

    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }

    log.Println("数据库连接成功")
    return DB
}

func CloseDB() {
    if DB != nil {
        sqlDB,err := DB.DB()
        if err != nil {
            log.Println("获取数据库连接失败:", err)
            return
        }
        sqlDB.Close()
        log.Println("数据库连接已关闭")
    }
}
//dbconfig.go
package config

import "fmt"

type DatabaseConfig struct {
    Host     string
    Port     int
    User     string
    Password string
    DBName   string
    SSLMode  string
    TimeZone string
}

func GetDBConfig() DatabaseConfig {
    return DatabaseConfig{
        Host:     "localhost",
        Port:     5432,
        User:     "postgres",
        Password: "wsh123456",
        DBName:   "ctf_test",
        SSLMode:  "disable",
        TimeZone: "Asia/Shanghai",
    }
}

func (c DatabaseConfig) ToDSN() string {
    return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s",
        c.Host, c.User, c.Password, c.DBName, c.Port, c.SSLMode, c.TimeZone)
}

img

curd(GORM Postgresql)

将 go 语言转换为 sql 语言,go 中的 Struct 映射为 db 中的 table,结构体的 field 映射为 column,

gorm 通过反射读取传入的数据类型,

img

create table

type User struct {
    gorm.Model
    Email `gorm:"uniqueIndex;not null"`
    Password `gorm:"not null"`
}

b.AutoMigrate(&User{})

insert data

user := USer{Email:"1027822561@qq.com",Password:"aaawsh2026"}
db.Create(&user)

使用 gorm.Model 或者列中有 ID 字段会被认定为主键,或者显示指定其他字段为主键

`gorm:"primaryKey"`

update data

// 通过主键 (ID) 查找相应数据
var user User
db.Frist(&user,1)
user.Password = "wsh20260201"
db.Save(&user)

delete data

db.Delete(&User{},1)

如果使用了 gorm.Model ,会打上已经删除的标签,当再次查找时,就是说数据还在库里。否则删除数据。

where 查询

DB.Where("username = ? AND age >= ?", "jinzhu", 18).Find(&users)
DB.Where("username = ?", "jinzhu").Or("age > ?", 18).Find(&users)

注意使用 ? 作为占位符,不使用 fmt.Sprintf 拼接 sql字符串。

事务回滚

有时候一组操作必须全部完成,例如转账等,因此如果中途有一部分失败,那么全部回滚是相对安全的操作.

tx := DB.Begin()

if err := tx.Create(&user).Error; err != nil {
    // 报错就回滚
    tx.Rollback()
    return
}

if err := tx.Create(&profile).Error; err != nil {
    tx.Rollback()
    return
}
//没问题 commit
tx.Commit()

不能使用全局的 DB ,因为 DB 一遇到执行语句就已经生效了 tx 可以回滚以及遇到 commit 才执行,为了避免在中途遇到 Panic

defer func() {
        // 如果中途发生 panic,强制回滚
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    if err := tx.Error; err != nil {
        return
    }

JWT 鉴权

​ 因为 jwt 无状态,所以我的设计是,用户登录时签发一个 token ,如果用户在 token 过期之前选择登出平台,那么我就将这个 token 存放在 redis 数据库中,做一个黑名单,当用户浏览器中仍然存在这个 token, 但是当他访问平台的时候就会被中间件检查到该 token 会禁止访问需要鉴权的页面,重定向到登录界面。

// 1. 签发 token
import "github.com/golang-jwt/jwt/v5"

type Claims struct {
    UserID int `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

userClaims := Claims{
    UserID: 111,
    Role: "user",
    RegisteredClaims: jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(24*time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
    }
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256,userClaims)
signedToken, err := token.SignedString([]byte("jwt-key"))
// 2. 验证 token

func KeyFuncFactory(secret []byte) jwt.Keyfunc {
    return func(token *jwt.Token) (inerface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, log.Fatalf("unexpected signing method:%v",token.Header["alg"])
        }
        return secret ,nil
    }
}

func InspectionToken(token string,secretKey string) (*Claims,error) {
    claims := &Claims{}
    _, err := jwt.ParseWithClaims(token,claims,KeyFuncFactory(secretKey))
    if err != nil {
        return nil,err
    }

    return claims,nil
}

redis

​ 内部有16 个 redisDb 对象,每个都实现了一个 dict ,dict 主要由哈希桶数组、哈希节点、双表结构组成,其中哈希桶数组存在两个,扩容时使用 ht[1].

​ 当我存入一个键值对,set key value 。key 会经过一些计算,根据计算结果绑定到哈希桶数组的一个槽位(内部存放一个指针)中,然后经过包装,(*key value *next) 这样一个结构存进槽位中。如果发现两个 key 占了同一个槽位,那就用到 *next 链表,头插法插入当前 key entry ,

​ hash % size ,如果 size 为 2 的 N 次方,则等价于 (hash & size-1),相对于 % ÷ ,cpu 能更快地解决 & | 这种位运算。

//redis 初始化

import (
    "context"
    "log"
    "os"

    "github.com/redis/go-redis/v9"
)

var RDB *redis.Client
var Ctx = context.Background()

func InitRedis() {
    addr := os.Getenv("REDIS_ADDR")
    password := os.Getenv("REDIS_PASSWORD")
    RDB = redis.NewClient(&redis.Options{
        Addr: addr,
        Password: password,
        DB: 0,
        // redis 16 个哈希表
    })
    if err := RDB.Ping(Ctx).Err(); err != nil {
        log.Fatalf("redis 连接失败,请重试:%v",err)
    }
}
// 使用 redis 做排行榜查询,避免频繁查询 postgresql 造成的压力
// 使用了 有序集合,内部维护了一个跳表,时间复杂度 O(log2(N))

var teams []model.Team

if err := DB.Find(&teams).Error; err != nil {
    log.Fatalf("获取排行榜数据失败:%v",err)
}

redisMembers := make([]redis.Z, 0, len(teams))

for _, team := range teams {
    redisMembers = append(redisMembers, redis.Z{
        Score:  float64(team.Score),
        Member: team.TeamName,
    })
}

if len(redisMembers) > 0 {
    if err := RDB.ZAdd(Ctx, "scoreboard", redisMembers...).Err(); err != nil {
        log.Printf("批量更新排行榜分数失败:%v", err)
    }
}
log.Printf("排行榜数据获取完成,共加载 %d 个团队数据",len(teams))

// 1. 优先从 redis 获取排行榜数据
results, err := config.RDB.ZRevRangeWithScores(config.Ctx, "scoreboard", 0, -1).Result()
results_count := len(results)
if err == nil && results_count > 0 {
    data := make([]model.Rank, results_count)
    for i, j := range results {
        data[i] = model.Rank{
            Rank:     i + 1,
            TeamName: j.Member.(string),
            Score:    j.Score,
        }
    }
    c.JSON(http.StatusOK, gin.H{
        "message": "获取排行榜数据成功",
        "data":    data,
    })
    return
}

// 2. 如果 redis 数据为空或查询失败,从数据库查询
var teams []model.Team
if err := config.DB.Order("score desc").Find(&teams).Error; err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{
        "message": "获取排行榜数据失败",
    })
    return
}

c.JSON(http.StatusOK, gin.H{
    "message": "获取排行榜数据成功",
    "data":    teams,
})

有一款可视化工具 redis-insight

redis.SetNX


k8s (TODO)

MQ

// 初始化 mq
package async_tasks

import (
    "backend/util/zaphelper"
    "os"

    "github.com/hibiken/asynq"

)

var AsynqClient *asynq.Client
var server *asynq.Server
var inspector *asynq.Inspector

// 任务类型常量
const (
    TypeStartContainer = "container:start"
    TypeStopContainer  = "container:stop"
    TypeSyncScore      = "rank:sync"
)

func InitMessageQuene() {
    addr := os.Getenv("REDIS_ADDR")
    if addr == "" {
        addr = "localhost:6379"
    }
    password := os.Getenv("REDIS_PASSWORD")
    if password == "" {
        password = "wsh_laribely"
    }

    redisOption := asynq.RedisClientOpt{
        Addr: addr,
        Password: password,
        DB: 1,
    }
    asynqConfig := asynq.Config{
        Concurrency: 30,
        Queues: map[string]int {
            "critical": 6,
            "commom": 3,
            "low": 1,
        },
        StrictPriority: true,
        Logger: zaphelper.NewZapLogger(zaphelper.Logger),
    }
    AsynqClient = asynq.NewClient(redisOption)
    inspector = asynq.NewInspector(redisOption)

    // 启动一个协程监听队列是否有任务
    go func() {
        server = asynq.NewServer(
            redisOption,
            asynqConfig,
        )
        mux := asynq.NewServeMux()
        mux.HandleFunc(TypeStartContainer, HandleStartContainerTask)
        mux.HandleFunc(TypeStopContainer, HandleStopContainerTask)
        mux.HandleFunc(TypeSyncScore, HandleSyncScoreTask)

        if err := server.Run(mux); err != nil {
            zaphelper.Sugar.Errorf("消息队列启动失败: %v", err)
        }
    }()
}
// mq 的应用

平台 k8s 耗时操作设计,handler 层做简单校验以及任务分发(将任务加到请求队列)并与数据库进行交互,然后 tasks 层去执行这些高耗时任务,记录成功失败日志。 cron_jobs 层定时查询实际执行状态并与数据库交互,或者发现一些没成功执行的任务,会去调用 tasks 继续重试执行。

img

大概逻辑 http_req --> web --> handler --> tasks --> 封装 task_type 和 payload --> server = asynq.NewServer() --> asynq.NewServeMux() --> server.Run(mux) --> mux 通过 task_type 分发到指定的 mux_handler --> 执行该请求

一些问题

func HashPassword(password string) string {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Printf("Error hashing password: %v", err)
        panic(err)
    }
    return string(bytes)
}

遇到 error 该怎么处理,现在只知道 panic 但是平台不稳定

**返回 error **

func HashPassword(password string) (string,error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Printf("Error hashing password: %v", err)
        return "", err
    }
    return string(bytes),nil
}
func RegisterUser(req model.RegisterRequest) (*model.User, error) {
    // 检查用户名是否存在
    var existUser model.User
    if err := database.DB.Where("username = ?", req.Username).First(&existUser).Error; err == nil {
        return nil, errors.New("用户名已存在")
    }

    // 检查邮箱是否存在
    if err := database.DB.Where("email = ?", req.Email).First(&existUser).Error; err == nil {
        return nil, errors.New("邮箱已被注册")
    }

    // 加密密码
    hashedPassword := HashPassword(req.Password)

    // 创建用户
    user := model.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
    }

    // 保存到数据库
    if err := database.DB.Create(&user).Error; err != nil {
        return nil, errors.New("用户创建失败")
    }

    return &user, nil
}

最后为什么返回 &user,nil ? 注册成功为什么返回这个。

数据库操作惯例返回指针

func HashPassword(password string) string {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Printf("Error hashing password: %v", err)
        panic(err)
    }
    return string(bytes)
}
func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

这个 hash 加密为什么没有密钥

内置盐值等

func LoginHandler(c *gin.Context) {
    var loginRequest model.LoginRequest

    if err := c.ShouldBindJSON(&loginRequest); err != nil {
        c.JSON(400, gin.H{"error": "请求参数错误: " + err.Error()})
        return
    }

    user, err := util.LoginUser(loginRequest)
    if err != nil {
        c.JSON(401, gin.H{"error": err.Error()})
        return
    }

    c.JSON(200, gin.H{
        "message": "登录成功",
        "user":    user,
    })
}

登录成功后返回 user 指针, *model.User 类型,这里能自动解析吗?为什么会自动解析?以及返回 user , user结构体中包含密码等敏感信息

c.JSON 会自动解引用


使用 json:"-" 保证 json 序列化时隐藏该字段

type User struct {
    gorm.Model
    Username string `gorm:"uniqueIndex;not null"`
    Email    string `gorm:"uniqueIndex;not null;email"`
    Password string `gorm:"not null;size:255" json:"-"`
}

这里询问 ai 之后发现需要添加标签 json 比较规范imgimg

Error Error() Err

error 接口,存在一个 Error() 方法,返回 string

err.Error() // 取出错误的文字描述

在 gorm 中

config.DB.Create(&user)
// 返回 *gorm.DB
// *gorm.DB 结构体中有一个字段为 Error ,实现了 error 接口,

go-redis 中

RDB.Ping(Ctx)
// 返回 *redis.StatusCmd
// 其中有一个方法叫 Err()
func (cmd *StatusCmd) Err() error { ... }