Golang API请求生命周期解析及其他相关内容

Golang API请求生命周期解析及其他相关内容 你好。

最近刚开始学习Go,对它还不是很熟悉。

当客户端访问一个API端点时,底层发生了什么?

我下面的理解是否正确: 请求 → 路由器 → 上下文 → 同步池 → 携带处理函数的Goroutine → 响应 → 释放 → 垃圾回收

有人能给我指出官方或其他可靠的资源吗?这个主题有详细讲解,但我一直没找到。

另外,关于数据库连接… 官方的教程说:

“将db设为全局变量简化了这个例子。在生产环境中,你应该避免使用全局变量,例如通过将变量传递给需要它的函数,或者将其包装在一个结构体中。”

我在找到的大多数文章中都看到了这一点,但没有一篇解释原因。

我的意思是,我会遵循官方的建议,但根本区别是什么?你可以把它包装在结构体里,但两个指针都指向同一个连接,而这个连接内部管理着连接池。

我通过Jmeter用两种方式测试了数千个请求,同时监控数据库中的活动和空闲连接,得到了相同的结果。

我使用了postgresql和sqlx,它是标准数据库库的包装器。所以我有点困惑。我遗漏了什么?

最后… 我偶然看到了这个。 如果你不介意的话,你能就你提到的“采用垂直方法的包解耦”写一两句话吗?这有点像垂直切片吗?

非常感谢


更多关于Golang API请求生命周期解析及其他相关内容的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您的澄清和解释。 这种垂直处理错误的方式非常巧妙,我能看出在较小规模的应用中可以如何融入它。

更多关于Golang API请求生命周期解析及其他相关内容的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


有人能给我指出详细讨论这个主题的官方或其他可靠资源吗?我找不到。

在没有亲自研究的情况下,无法对你的具体调查发表评论,但查看源代码应该会有所帮助:

  1. http 包 - net/http - Go 包
  2. https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/net/http/

新的文档网站页面底部有源代码。以后可以随时探索。

我的意思是,我会遵循官方建议,但根本区别是什么?你可以把它包装在结构体里,但两个指针都指向同一个连接,内部有连接池管理。

这更多是关于变量的内存可访问性,而不是性能。它们不应该有任何区别,因为两者都是在访问一个指针。

在生产系统中,你希望对“魔法”有更多的控制。一个全局变量可以被任何函数访问(取决于其访问权限,包内或包外),因此任何人都可能对它进行意外的修改,特别是当它涉及到一个关键组件:你的数据库访问时。这意味着,与其这样做:

var DB *sql.DB  // 让我们把它变得更糟,将这个变量设为公开可访问,然后让 func Rogue 在其自己的 goroutine 中修改它
...

你可以这样做:

func SaveData(db *sql.DB, ...) {
    ...
}

func main() {
    ...
    db, err = sql.Open("mysql", cfg.FormatDSN())
    ...
    SaveData(db, ...)
}

对于后者,你对数据库内存对象有更严格的控制(例如,哪个函数在某一时刻负责覆盖/访问该数据库内存对象)。同时,在任何时候,你都知道可以安全地丢弃 db(考虑到并发扩展的情况)。

另外,你需要注意这在 Go 中是可行的,所以函数内的变量命名可能会产生误导,把事情搞乱:

package main

import (
	"fmt"
)

func counter(fmt int) int {
	return fmt + fmt
}

func main() {
	fmt.Println(counter(5))
}

Playground: Go Playground - Go 编程语言

如果你不介意的话,能否就你提到的“采用垂直方法的包解耦”写一两句话?这有点像垂直切片吗?

这绝对不是一种数据类型。这只是一种打包代码的策略,目的是尽可能减少错误检查。请记住,这只是一种观点,不是规则。

要做到这一点,你需要:

  1. 在开发中使用 internal/ 包。
  2. 非常擅长使用指针。
  3. 始终使用 struct 类型来构建包数据。
  4. 擅长“函数作为值”。

技巧在于:

  1. 将一个日志记录器结构体对象传递到中间包中(作为结构体的一个元素,而不是全局变量),用于记录冗长的错误消息,而不是一路返回到应用层函数。
  2. 确保日志记录函数能够处理可选的日志记录,因为并非每个人都实践这种策略。
  3. 利用指针作为返回值(基本上是用 nil 表示错误的输出)。然后问自己:这个错误信息对你的高层函数“客户”有用吗: 3.1. 如果“是”,那么你返回错误对象(通常是“否”。见[1]) 3.2. 否则,只需记录它并返回 nil 作为值。
  4. 重要:中间包必须可复用于其他应用开发,因此随着时间的推移,它会从你的 internal/ 包中成熟起来。

[1] - 之所以说“否”,是因为应用层包(最高层函数)有自己专用的错误信息呈现给用户(参见:GitHub - tomarrell/wrapcheck: 一个 Go 语言检查器,用于检查外部包的错误是否被包装)。因此,这些错误信息是针对内部应用开发人员的,所以运行时日志记录器比错误升级更有意义,这极大地减少了层层向上的错误检查。

关于Go API请求生命周期的解析

你的理解基本正确,但可以更精确地描述。一个典型的HTTP请求在Go中的处理流程如下:

请求 → 监听器 → 连接池 → 路由器 → 上下文创建 → 处理器函数(在Goroutine中执行) → 响应 → 连接复用/关闭 → 垃圾回收

详细流程示例:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        // 1. 请求到达,创建新的上下文
        ctx := r.Context()
        
        // 2. 在独立的goroutine中处理请求
        go func() {
            select {
            case <-ctx.Done():
                // 客户端取消请求
                return
            case <-time.After(2 * time.Second):
                // 模拟处理逻辑
                w.Write([]byte("响应数据"))
            }
        }()
    })
    
    // 3. 服务器监听端口
    http.ListenAndServe(":8080", nil)
}

官方资源参考:

关于数据库连接全局变量的问题

你观察到的现象是正确的,但官方建议避免全局变量有更深层的原因:

// 方式1:全局变量(不推荐)
var db *sql.DB

func init() {
    db, _ = sql.Open("postgres", "connection_string")
}

// 方式2:依赖注入(推荐)
type App struct {
    db *sql.DB
}

func NewApp(db *sql.DB) *App {
    return &App{db: db}
}

func (a *App) GetUser(id int) (*User, error) {
    var user User
    err := a.db.QueryRow("SELECT * FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name)
    return &user, err
}

根本区别:

  1. 可测试性:使用结构体包装可以轻松模拟数据库进行单元测试
  2. 并发安全:虽然连接池内部是线程安全的,但全局变量在复杂应用中可能导致意外的状态共享
  3. 生命周期管理:结构体可以明确控制数据库连接的初始化和关闭
  4. 配置灵活性:不同环境可以使用不同的数据库配置

关于"垂直方法的包解耦"

这确实类似于垂直切片架构。示例:

// 传统分层架构(水平分层)
// dao/user_dao.go
// service/user_service.go  
// controller/user_controller.go

// 垂直解耦架构(按功能模块组织)
// user/
//   ├── handler.go      // HTTP处理器
//   ├── service.go      // 业务逻辑
//   ├── repository.go   // 数据访问
//   └── types.go        // 类型定义

// order/
//   ├── handler.go
//   ├── service.go
//   └── repository.go

// 每个模块独立,减少包间耦合
type UserModule struct {
    repo    UserRepository
    service UserService
}

func (m *UserModule) RegisterRoutes(r *mux.Router) {
    r.HandleFunc("/users/{id}", m.GetUser)
}

// 泛型在此架构中的应用示例
type Repository[T any] interface {
    FindByID(id int) (*T, error)
    Save(entity *T) error
}

type UserRepository struct {
    Repository[User]
}

这种架构的优势在于:

  1. 功能模块高度内聚
  2. 模块间依赖关系清晰
  3. 便于独立开发和测试
  4. 泛型可以创建通用的数据访问层

数据库连接池的管理在不同方式下表现相似,因为底层使用的是同一个sql.DB连接池。差异主要体现在代码组织、测试便利性和架构清晰度上。

回到顶部