Files
StudyNote/语言/GO/go.md
2026-02-13 23:38:38 +08:00

22 KiB
Raw Permalink Blame History

路由管理

goframe

如何将函数与路径绑定在一起

    s.BindHandler("/", func(r *ghttp.Request) {
        r.Response.Write("Hello World!")
    })

BindHandler("路径",函数)

通过Server对象的BindHandler方法绑定路由以及路由函数。在本示例中,我们绑定了/路由,并指定路由函数返回Hello World

当然 我们也可以写一个函数 来代替func

批量绑定控制器

s.BindObject("路径", obj)

s.BindObject("/user", user.Controller{})

这样就可以一次性 将user包下面的方法给绑定到对应的路径下面

  • BindObject(path, obj) 中,基础路径 path 需要显式传入(比如 /user),每次绑定都要手动指定。

GoFrame 的 Bind 方法为了兼容 “值类型” 和 “指针类型” 的控制器实例,内部做了自动转换处理(统一转为指针类型),以确保指针接收者的方法能被正确识别和绑定。这就是为什么传入 Hello{}NewHello()(返回 *Hello)会得到相同结果的底层原因。

分组

RouterGroup.Bind(obj) 中,基础路径来自当前路由组RouterGroup预先定义的路径(比如 group := s.Group("/api") 中,/api 就是基础路径),后续绑定无需重复写,直接继承组的路径。

			s.Group("/api", func(group *ghttp.RouterGroup) {
				group.Middleware(ghttp.MiddlewareHandlerResponse)
				group.Group("/hello", func(helloGroup *ghttp.RouterGroup) {
					helloGroup.Bind(hello.NewHello())
				})

				group.Group("/user", func(userGroup *ghttp.RouterGroup) {
					userGroup.Bind(user.Controller{})
				})
           //分成了两个组 hello和user
           //但是 他们都在 api的下面 所以是api/user  api/hello
			})

规范路由

GoFrame中提供了规范化的路由注册方式注册方法如下

func Handler(ctx context.Context, req *Request) (res *Response, err error)

其中RequestResponse为自定义的结构体。

通过如下方式指定请求方法与路径

type HelloReq struct {
    g.Meta `path:"/hello" method:"get"`
}

获取请求参数Req

func (c *Hello) Param(ctx context.Context, req *hello.ParamReq) (res *hello.ParamRes, err error) {
	r := g.RequestFromCtx(ctx)
	name := r.GetQuery("name")
    data := r.GetQueryMap(g.Map{"name": name, "age": 2}) 
	r.Response.Writeln(name.String() + "2778")
	return
}

g.RequestFromCtx(ctx) --> 从上下文中提取当前 HTTP 请求对象 *ghttp.Request

在 GoFrame 框架中,每个 HTTP 请求处理时,框架会自动将当前请求的 *ghttp.Request 对象存入上下文 ctx 中,方便在函数调用链中传递和获取(无需显式在函数参数中传递 *ghttp.Request)。

ctx context.Context -->是传递请求生命周期和元数据的载体。

  • 里面包含了当前请求的元信息(如请求 ID、超时控制等g.RequestFromCtx 通过这个上下文 “找到” 对应的请求实例。
  • 示例中的意义:在 Param 方法中,通过 g.RequestFromCtx(ctx) 从上下文获取当前请求对象 r,后续可以通过 r 操作请求(如获取参数、写入响应等)。

r.GetQuery("name") -->获取 URL 查询参数中 key 为 "name" 的值。

data := r.GetQueryMap(g.Map{"name": name, "age": 2}) 这个name 和2 就是默认值 让这个其实获取的就是{"age":"1231","name":"啊啊啊","rwqe":"123123"} 这种格式的 当然 只要有 就写上去 即使是你不想要的

Get方法

  • GetQuery\* 系列方法(如 GetQueryGetQueryMap):仅用于获取 URL 查询参数(对应 HTTP 的 GET 方法参数)。

  • GetForm\* 系列方法(如 GetFormGetFormMap):用于获取 POST 表单参数(如 application/x-www-form-urlencodedmultipart/form-data 类型的 POST 数据)。

  • GetJson\* 系列方法:用于获取 POST JSON 数据application/json 类型的 POST 数据)。

  • getRouterMap 用于获取 动态路由 {}里面的数据

从req中直接取数值 goland推荐的


func (c *Hello) Param(ctx context.Context, req *hello.ParamReq) (res *hello.ParamRes, err error) {
	r := g.RequestFromCtx(ctx)
	r.Response.Write(req)
	return
}

  • 无需关心参数的原始提交方式GET/POST/JSON 自动兼容);
  • 自动过滤无关参数(只保留 ParamReq 中定义的字段);
  • 支持通过结构体标签(如 v:"required")做参数校验(框架会自动返回校验错误)。

自己写一个结构体 取接住

func (c *Hello) Param(ctx context.Context, req *hello.ParamReq) (res *hello.ParamRes, err error) {
	r := g.RequestFromCtx(ctx)
	type User struct {
		Name string
		Age  int
	}
	var u User
	err = r.ParseForm(&u)
	if err != nil {
		r.Response.Write(u)
	}
	return
}

p d

type ParamReq struct{
	Name string `p:"name" d:"无名"`
    Age  int `json:age`
}
如果 前端不是name 但是你又不想新建一个字段  那么就用 p 起一个别名 也鞥适用于name
d 默认值
josn 可以让返回值返回为json的样式

相应Res

r.Response.Writeln() 最常用的 直接返回文字 结构体 甚至是html标签

r.Response.

  • Writeln 换行
  • Write 不换行
  • Writef 可以有占位符
  • WriteJson()

数据库

前期准备

配置文件

manifest/config/config.yaml

server:
    address: ":8000"         # 服务监听端口
    openapiPath: "/api.json" # OpenAPI接口文档地址
    swaggerPath: "/swagger"  # 内置SwaggerUI展示地址
 
database:
  default:
    link:   "mysql:root:12345678@tcp(127.0.0.1:3306)/star?loc=Local"
    debug:  true

CRUD

查询

g.Model("表名") 返回是模型对象

g.Model("products") 会创建一个与 products 表绑定的模型对象(后续所有操作都针对该表)。

获得到 g.MOdel()以后 比如 query= g.MOdel(“一个表名”)就可以通过

products, err := query.One() 查询一个 返回值是 查询的返回值 和错误数量

  • qu.One()查询一条
  • qu.All() 查全
  • qu.Fields("id", "vend_id").All() 查询那两个字段
  • qu.FieldsEX("id", "vend_id").All() 查询 除了那两个字段
  • qu.Value("id") 查id这一个字段的一个数据
  • qu.Array("id")查id这一个字段的所有
  • qu.Count 用于查询并返回记录数。

如果对于同一个g.Model() 的对象 query 使用的话 就会在原有的基础上 查询 (上一条的查询语句 会影响到吓一跳)

所以我们的解决思路就是 ---> 每次都查询的是g.Model().查询的内容

Where

  • qu.Where("id", 1).All() --> 查询id=1

  • qu.Where("id=2").All() ---> 查询id=2

  • qu.WhereGT("id",2).All() ---> 查询id> GTE >=

  • LT -->< LTE--> < =

  • Where("level=? OR money >=?", 1, 1000000) 使用?作为占位符

  • Where("level", 1).WhereOr("money >=", 1000000) 或者直接使用WhereOr来表示or

Order

pro, error := qu.WhereGTE("id", 2).OrderAsc("prod_price").All()

Group

pro, error := qu.WhereGTE("id", 2).Group("prod_price").All()

分页

pro, error := qu.Limit(1,3).All()

pro, error := qu.Page(3,5).All() 第三页 每页五个

结构体映射

	type Products struct {
		Id        int
		VendId    int
		ProdName  string
		ProdPrice float64
	}
	var products *Products
	_ = g.Model("products").Scan(&products) 其实这个的返回值是error
	req.Response.WriteJson(products)

Scan 的是& 因为你是直接把这个对象给改了

然后有一点

var products *Products 和var products Products的返回结果是一样的

但是在查询查不到的地方 用指针的是Null 而用对象的是返回语句 “sqlxxx查不到”

把var products *Products 改成var products 【】*Products 就是查询所有的了

插入

insert

result, err := qu.Data(data).Insert()
等价的两句话
result, err := qu.Insert(data)

返回值

{
    "Result": {
        "Locker": {}
    },
    "Affected": 1 影响的数量
}

Replace

result, err := qu.Replace(data)

根insert的区别就在于 replace是替换 就是即使是主键冲突 也可以改 当然 也可以添加

而insert就是会报错

3.save 如果写入的数据中存在主键或者唯一索引时,更新原有数据,否则写入一条新数据。

批量插入

	type Products struct {
		Id        int
		VendId    int
		ProdName  string
		ProdPrice float64
	}
	data := g.List{//一系列键值对集合
		{"vend_id": 10, "prod_id": 11, "prod_name": "商品2", "prod_price": 100.0},
		{"vend_id": 20, "prod_id": 11, "prod_name": "商品2", "prod_price": 100.0},
		{"vend_id": 40, "prod_id": 11, "prod_name": "商品2", "prod_price": 100.0},
		{"vend_id": 40, "prod_id": 11, "prod_name": "商品2", "prod_price": 100.0},
	}
	result, err := qu.Insert(data)
	if err == nil {
		req.Response.WriteJson(result)
	}

或者是g.Array 但是里面就要存g.map或者结构体

    data := g.Array{//存g.map或者结构体
        g.Map{
            "vend_id":    10,       // 正确字段名(避免拼写错误)
            "prod_name":  "商品A",   // 必填字段
            "prod_price": 99.9,     // 价格字段
        },
        g.Map{
            "vend_id":    10,
            "prod_name":  "商品B",
            "prod_price": 199.9,
        },
    }

Update

Update

// UPDATE `user` SET `status`=1 WHERE `status`=0 ORDER BY `login_time` asc LIMIT 10
g.Model("user").Data("status", 1).Order("login_time asc").Where("status", 0).Limit(10).Update()

//或者直接使用update
g.Model("user").Update("status=1", 1)

用于数据的更新,往往需要结合 DataWhere 方法共同使用。 Data 方法用于指定需要更新的数据, Where 方法用于指定更新的条件范围。同时, Update 方法也支持直接给定数据和条件参数。

Decremet&Increment

result, err := qu.WhereLTE("id", 5).Increment("prod_price",10) 
//让那些 id《=5的 加10的price

删除

Delete()

g.Model("user").Where("uid", 10).Delete()

事务

普通的写法

	db := g.DB()//是 g 提供的数据库操作方法,用于获取一个数据库连接对象
//如果有上下文 也就那个 ctx context.Context 可以直接写成 db.Begin(ctx)  这个req.Context()==ctx
	if tx, err := db.Begin(req.Context()); err == nil {
		r, err := tx.Save("user", g.Map{
			"id":   1,
			"name": "john",
		})
		if err == nil {
			tx.Commit()
		} else {
			tx.Rollback()
		}
		fmt.Println(r)
	}
data := new(Products)
	tx, err := g.DB().Begin(req.Context())//开启事务
	if err != nil {
		req.Response.WritelnExit("发生错误: " + err.Error())//事务开启时失败
	}

	md := tx.Model("book")
	result, err := md.Insert(data)

	if err == nil {//插入失败
		tx.Commit()
		req.Response.WriteJson(result)
	} else {
		tx.Rollback()
		req.Response.Writeln("发生错误: " + err.Error())
	}

闭包操作

	g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
		data := new(Products)
		tx, err := g.DB().Begin(req.Context())
		if err != nil {
			req.Response.WritelnExit("发生错误: " + err.Error())
		}
		md := tx.Model("book")
		result, err := md.Insert(data)

		if err == nil {
			req.Response.WriteJson(result)
		} else {
			req.Response.Writeln("发生错误: " + err.Error())
		}
		return err
	})

这个方法 就是通过判断error方法 来实现对于自动提交和rollback

原生sql

你想听我原生家庭的故事吗

sql := "INSERT INTO `book` (`name`, `author`, `price`) VALUES (?, ?, ?)"
db := g.DB()
result, err := db.Exec(req.Context(), sql, g.Array{3, 7})

DAO

自动生成的 先去hack/config.yml中进行

然后在执行 gf gen dao

/internal/dao 数据操作对象 通过对象方式访问底层数据源,底层基于 ORM 组件实现。往往需要结合 entitydo 共同使用。该目录下的文件开发者可扩展修改。
/internal/model/do 数据转换模型 数据转换模型用于业务模型到数据模型的转换,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。关于 do 文件的介绍请参考: - 数据模型与业务模型 - DAO-工程痛点及改进 - 利用指针属性和do对象实现灵活的修改接口
/internal/model/entity 数据模型 数据模型由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。

do 在插入的时候不会将其他的给顶替掉

而 entity 的却会

连表查询

image-20251114192410449

在entity表里面 写入其他表的指针 然后 json表示的是 在关联查出的名字 然后orm表示的是链表的 whit"xx"="xx"

db := g.DB()
db.With(entity.Dept{}).Scan(&emp)

db.With(entity.Dept{} 也就是entity表里面的构造器 也是要链表 ).Scan(&emp 是这个表的对象 也是)

Service层

作用:

  • 定义接口
  • 定义接口变更
  • 定义一个获取接口实例的函数
  • 定义一个接口实现的注册方式

dcdevicegroup.go

dc_device_group.go

dc_device_group.go

WebSocket

image-20251207094527958

WebSocket 通信始于 HTTP 握手,客户端通过 Upgrade 请求头将 HTTP 连接升级为 WebSocket 连接。握手成功后,双方通过消息帧(文本、字节或控制帧如 ping/pong进行通信。控制帧用于维护连接例如 ping/pong 用于检测连接是否存活。

type Conn:网络连接的通用抽象

Conn是Go语言定义的网络连接接口涵盖了所有网络类型TCP、UDP、Unix域套接字等的公共操作

>websocket升级器

// wsUpgrader WebSocket 升级器配置
var wsUpgrader = websocket.Upgrader{
	// CheckOrigin 允许任何来源(开发环境)
	// 生产环境中应该实现适当的来源检查以确保安全
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
	// Error 处理升级失败的错误
	Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
		// 在这里实现错误处理逻辑
		g.Log().Errorf(r.Context(), "WebSocket upgrade error: %v", reason)
	},
}

原生

package websocket


import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gorilla/websocket"
)

// WebSocketController WebSocket 控制器
type WebSocketController struct{}

// New 创建 WebSocket 控制器实例
func New() *WebSocketController {
	return &WebSocketController{}
}

// wsUpgrader WebSocket 升级器配置
var wsUpgrader = websocket.Upgrader{
	// CheckOrigin 允许任何来源(开发环境)
	// 生产环境中应该实现适当的来源检查以确保安全
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
	// Error 处理升级失败的错误
	Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
		// 在这里实现错误处理逻辑
		g.Log().Errorf(r.Context(), "WebSocket upgrade error: %v", reason)
	},
}

// Echo WebSocket Echo 服务器处理器
// 路径: /ws
func (c *WebSocketController) Echo(r *ghttp.Request) {
	// 将 HTTP 连接升级为 WebSocket
	ws, err := wsUpgrader.Upgrade(r.Response.Writer, r.Request, nil)
	if err != nil {
		r.Response.Write(err.Error())
		return
	}
	defer ws.Close()

	// 获取请求上下文用于日志记录
	var ctx = r.Context()
	logger := g.Log()

	// 消息处理循环
	for {
		// 读取传入的 WebSocket 消息
		msgType, msg, err := ws.ReadMessage()
		if err != nil {
			break // 连接关闭或发生错误
		}
		// 记录接收到的消息
		logger.Infof(ctx, "received message: %s", msg)
		// 将消息回显给客户端
		if err = ws.WriteMessage(msgType, msg); err != nil {
			break // 写入消息时出错
		}
	}
	// 记录连接关闭
	logger.Info(ctx, "websocket connection closed")
}

使用gorilla/websocket

package main

import (
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

// 定义 WebSocket 升级器
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

// 存储所有客户端连接
var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan string) // 广播消息通道

// 处理 WebSocket 连接
func handleConnections(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("upgrade error: %v", err)
        return
    }
    defer ws.Close()

    // 注册新客户端
    clients[ws] = true

    for {
        var msg string
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("read error: %v", err)
            delete(clients, ws)
            break
        }
        // 将消息发送到广播通道
        broadcast <- msg
    }
}

// 广播消息给所有客户端
func handleBroadcast() {
    for {
        msg := <-broadcast
        for client := range clients {
            err := client.WriteJSON(msg)
            if err != nil {
                log.Printf("write error: %v", err)
                client.Close()
                delete(clients, ws)
            }
        }
    }
}

func main() {
    http.HandleFunc("/ws", handleConnections)
    go handleBroadcast() // 启动广播 goroutine
    log.Fatal(http.ListenAndServe(":8080", nil))
}


  • clients 存储所有活跃连接,broadcast 通道用于消息分发。
  • 每个客户端连接运行在独立 goroutine 中,处理消息读取。
  • handleBroadcast 循环监听广播通道,将消息推送给所有客户端。

并发处理

package main

import (
    "log"
    "net/http"
    "sync"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
var clients = make(map[*websocket.Conn]bool)
var mutex = sync.RWMutex{} // 保护 clients 并发访问

func handleConnections(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("upgrade error: %v", err)
        return
    }

    // 注册客户端
    mutex.Lock()
    clients[ws] = true
    mutex.Unlock()

    defer func() {
        mutex.Lock()
        delete(clients, ws)
        mutex.Unlock()
        ws.Close()
    }()

    for {
        var msg string
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("read error: %v", err)
            break
        }
        // 广播消息(简化示例)
        mutex.RLock()
        for client := range clients {
            client.WriteJSON(msg)
        }
        mutex.RUnlock()
    }
}

性能优化 通过临时的bufferPool

var bufferPool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func readMessage(ws *websocket.Conn) ([]byte, error) {
    buf := bufferPool.Get().([]byte)
    defer bufferPool.Put(buf)
    _, data, err := ws.ReadMessage()
    return data, err
}

心跳

func handlePingPong(ws *websocket.Conn) {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
	
    for {
        select {
        case <-ticker.C:
            if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
                log.Println("ping error:", err)
                return
            }
        }
    }
}

多房间聊天

type Room struct {
    clients map[*websocket.Conn]bool
    broadcast chan string
}

func (r *Room) handleBroadcast() {
    for msg := range r.broadcast {
        for client := range r.clients {
            client.WriteJSON(msg)
        }
    }
}

GC

垃圾回收的过程 分为两个半独立的组件

赋值器 & 回收器

赋值器:这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。

回收器:执行垃圾回收的代码

根对象 --> 垃圾回收器在标记的过程中最先检查的对象

  • 全局变量 :程序在编译期就能确定的那些存在于程序整个生命周期的变量
  • 执行栈 每个goroutine 都包含自己的执行栈 这些执行栈包含栈上的变量以及指向分配的堆内存区块的指针
  • 寄存器 寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块

GC方法 追踪式GC

从根对象出发 根据对象之间的引用信息 一步步推导知道扫描完整个堆并且确定要保存的对象 从而确定回收的对象

三色标记法

三色抽象规定了三种不同类型的对象,并用不同的颜色相称

  • 白色对象**(可能死亡)**:未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
  • 灰色对象**(波面)**:已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
  • 黑色对象**(确定存活)**:已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
image-20251220221017875