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 := 0for i := 0; i < 5 ; i++ { sum += i}fmt.Println(sum)if
不用带括号 () ,
i := 0if 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.gogo 的导入包是
import "projectName/folderName"
folderName.FuncNamegin 框架
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)
}}

闭包
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 数据库,或者也可以先连接默认的库 postgres
docker run --name postgres \-e POSTGRES_PASSWORD=wsh123456 \-p 5432:5432 \-d postgres:15-alpinepackage 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("数据库连接已关闭") }}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)}
curd(GORM Postgresql)
将 go 语言转换为 sql 语言,go 中的 Struct 映射为 db 中的 table,结构体的 field 映射为 column,
gorm 通过反射读取传入的数据类型,

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 Userdb.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}//没问题 committx.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. 签发 tokenimport "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.Clientvar 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.Teamif 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
// 初始化 mqpackage async_tasks
import ( "backend/util/zaphelper" "os"
"github.com/hibiken/asynq"
)
var AsynqClient *asynq.Clientvar server *asynq.Servervar 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 继续重试执行。

大概逻辑 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 比较规范

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 { ... }