Go 学习笔记一

gomkiri 发布于 2025-11-26 76 次阅读


AI 摘要

探索Go语言核心特性:从简洁的go mod依赖管理到原生HTTP服务,再到灵活的中间件模式。揭秘如何用ServeMux实现多路由表,以及Go独特的可见性规则。适合Java开发者快速掌握Go编程精髓。

Go Mod

go Mod 是 Go 项目的包管理器,其内容十分简洁,通常就是 包名 + Go 版本 + 引入依赖列表,一个简单示例:

 // 1. Module 名字 (相当于 GroupID + ArtifactID)
 // 通常直接用 GitHub 地址,方便别人 import
 module github.com/gomkiri/my-project
 ​
 // 2. Go 版本 (相当于 <java.version>)
 go 1.23.0
 ​
 // 3. 依赖列表 (相当于 <dependencies>)
 require (
  // 格式:包名 版本号
  github.com/gin-gonic/gin v1.9.1
  github.com/go-sql-driver/mysql v1.7.1 // indirect
 )

// indirect 表示这是一个间接依赖(你引用的包引用的包),Go 会自动记录下来,通常不用管。

常用命令

go mod init <Module Name>

用于对一个 go 项目进行初始化

go get <Moudule Name>

用于导出/更新依赖

go mod tidy

自动对项目中的依赖进行更新:将少的自动加上,将多的自动删除。

go.sum

当我们使用 go mod init 后,会自动生成一个 go.sum 这是一个自校验文件,校验程序完整性,我们在开发过程中不用管他,但是注意在项目管理时一定不能将这个文件给忽略掉!

序列化

go 语言为我们提供了 encoding/json 包,其作用就是进行序列化和反序列化的操作。

为结构题字段设置序列化名称

 type User struct {
  ID int 'json:"id"'
  Name string 'json:"name"'
  Email string 'json:"email"'
 }

这个操作就类似于我们在使用 java 在 bean 的字段上面加 @JsonProperty 注解一样,设置其在序列化时对应的key

完成序列化

完成序列化通过 json.Marshal()来实现,定义为*func* Marshal(v any) ([]*byte*, *error*),也就是说可以通过第二个参数来获取并手动处理异常。

http

原生的 http 操作是通过 net/http 包完成的,使用流程包括:

注册路由

使用 http.HandleFunc 来完成路由注册,第一个参数为要响应的路由,第二个参数为自定义处理方法。

 http.HandleFunc("/users", usersHandler)

在 1.22 以后的 Go 版本中,第一个参数不仅可以定义路由,还可以定义请求方式:"GET /users"作为第一个参数是完全合法的!不仅如此,还有可以接受路径传参:GET /users/{id} 也是完全合法,而我们在使用时只需要:id := r.PathValue(id)即可获取路径参数。

自定义的处理方法可以接受两个参数 w http.ResponseWriter, r *http.Request ,分别代表访问参数和响应参数,这也是一个跟 java 很不同的点,我们想要响应的内容不需要 return ,而是直接写入到 w 之中即可,例如:

 // getUser 是一个新的 handler,用于返回用户信息的 JSON。
 func getUser(w http.ResponseWriter, r *http.Request) {
  // 1. 创建一个 User 实例
  user := User{
  ID: 1,
  Name: "Gopher",
  Email: "gopher@example.com",
  }
 ​
  // 2. 序列化为 JSON
  jsonResponse, err := json.Marshal(user)
  if err != nil {
  http.Error(w, err.Error(), http.StatusInternalServerError)
  return
  }
 ​
  // 3. 设置响应头
  // 在写入响应体之前,必须设置好 Header。
  // 这里我们告诉客户端,返回的内容是 JSON 格式。
  w.Header().Set("Content-Type", "application/json")
 ​
  // 4. 写入响应
  w.Write(jsonResponse)
 }

但是注册路由并不会自动启动 http 服务器,我们还需要使用 ListenAndServe 方法:

     // 第一个参数是端口号
  err := http.ListenAndServe(":8080", nil)
  if err != nil {
  log.Fatal(err)
  }

ServeMux

在上面注册路由的时候,我们将第二个参数设置为了nil ,这里其实可以放一个 ServeMux,mux 相当于是一个局部路由表,而直接使用 http.HandleFunc 的方式设置的路由是全局路由表,使用局部路由表有更多的好处:

  1. 避免全局状态 (No Global State):
    • 全局变量(默认 ServeMux 就是一个全局变量)在大型应用中往往会导致不可预测的行为和难以调试的问题。
    • 自定义 mux 避免了这种全局状态,让你的路由配置更加独立和可控。
  2. 更好的封装和测试 (Better Encapsulation & Testability):
    • 你可以为应用的不同部分创建不同的 ServeMux 实例,实现路由的模块化。
    • 在测试时,你可以很容易地创建一个 ServeMux 实例,注册你想要测试的 Handler,然后对它进行独立的测试,而不会受到其他路由的影响。
  3. 中间件的基础 (Foundation for Middleware):
    • 尽管我们还没有讲到中间件,但自定义 ServeMux 是实现中间件(在请求到达你的业务逻辑之前或之后执行一些通用操作,如日志、认证)的强大基础。你可以将中间件包装在你的 mux 外部。
  4. 清晰的应用架构 (Clearer Architecture):
    • 显式地创建和配置路由器,让你的应用结构更加清晰,更容易理解和维护。

同时 go语言也支持一个程序中注册多套路由,原生的实现方式有二:

方案一

使用 goroutine 开启多个线程监听多个端口

 func main(){
  // 定义第一套路由表并在协程中开启监听
  publicMux := http.NewServeMux()
  publicMux.HandleFunc("/hello",handlePuvlic)
  go func(){
  log.Println("在8080端口对用户提供服务")
  if err := http.ListenAndServe(":8080",publicMux) ; err != nil{
  log.Fatal(err)
  }
  }
 
  // 定义第二套路由表并在协程中开启监听
  adminMux := http.NewServeMux()
  adminMux.HandleFunc("/satus", HandelAdminStats)
  go func(){
  log.Println("在8081端口对管理员提供服务")
  if err := http.ListenAndServe(":8081",adminMux) ; err != nil{
  log.Fatal(err)
  }
  }
 }

方案二

使用多个子路由表聚合为一个路由,然后在一个端口对外提供服务,这个使用场景尝尝用于分版本来管理 API:

 func main() {
  // --- V1 版本的路由 ---
  v1Mux := http.NewServeMux()
  v1Mux.HandleFunc("/users", handleV1Users) // 处理 /api/v1/users
  // --- V2 版本的路由 ---
  v2Mux := http.NewServeMux()
  v2Mux.HandleFunc("/users", handleV2Users) // 处理 /api/v2/users
 
  // --- 主路由器 ---
  mainMux := http.NewServeMux()
  // 将所有 /api/v1/ 开头的请求都交给 v1Mux 处理
  mainMux.Handle("/api/v1/", http.StripPrefix("/api/v1", v1Mux))
  // 将所有 /api/v2/ 开头的请求都交给 v2Mux 处理
  mainMux.Handle("/api/v2/", http.StripPrefix("/api/v2", v2Mux))
 
  log.Println("Starting server on port 8080 with API versions v1 and v2")
  if err := http.ListenAndServe(":8080", mainMux); err != nil {
  log.Fatal(err)
  }
 }

分包

虽然 go 语言并没有一个强制性的目录结构,但是在社区中通常推荐使用一种叫做 Standard Go Project Layout 的规范,地址:https://github.com/golang-standards/project-layout/blob/master/README_zh.md,大体结构如下:

 my-shop/
 ├── cmd/ # 程序的入口 (相当于 Application.java)
 │ └── server/
 │ └── main.go # 这里只做启动逻辑,加载配置,初始化路由
 ├── internal/ # 【私有代码】核心业务逻辑 (不允许被外部项目 import)
 │ ├── handler/ # 相当于 Controller 层 (处理 HTTP 请求)
 │ ├── service/ # 相当于 Service 层 (业务逻辑)
 │ ├── repository/ # 相当于 DAO/Mapper 层 (数据库操作)
 │ ├── model/ # 相当于 Entity/POJO (数据库模型)
 │ └── pkg/ # 内部使用的工具类
 ├── pkg/ # 【公共代码】可以被外部引用的库 (比如通用的加密工具)
 ├── api/ # OpenAPI/Swagger 文档定义
 ├── configs/ # 配置文件 (config.yaml)
 ├── go.mod # 依赖管理
 └── go.sum

并且该规范还强调了,不应该使用 src 包来存放代码,这跟历史版本中使用的 $GOPATH 有关

可见性

与 Java 中直接使用关键字来定义可见性有很大的不同,go 的可见性规则为:

首字母大写的标识符是导出的(public),可以被其他包访问;首字母小写的标识符是未导出的(pirvate),只能在当前包内访问。

并且这个霸道的规则适用于 go 语言的几乎所有内容。这就要求我们在定义一个结构体的时候,不仅结构体名字是首字母大写的,就连其中的字段和他的方法,也要首字母大写才能保证其可见性。

同时这用引出来一个问题:既然 go 的封装特性如此简化,我们还需要严格遵守 java 的那一套权限管理(比如 java Bean 的实现风格)

在 Go 里,除非你有非常明确的理由(如安全性、锁、验证逻辑)要隐藏字段,否则默认都是公开的。

也就是说,只要没有特别重要的原因,就不需要,直接将字段首字母写成大写就是了,要使用就直接用 . 的方式用,不要担心,造就完了。

如果是要实现 get/set 的特殊场景,在实现时注意 Go 的规范:

  • Getter : 不用 get 前缀。比如字段叫 size,方法叫 Size(),而不是 getSize()
  • Setter : 使用 set 前缀。比如 SetSize()

Go 的中间件模式

go 的中间件模式好像是 java 中的请求过滤器,但是实现非常简单:

 err := http.ListenAndServe(":8080", middleware.Logging(mux))

可以看到 mux 对象外又包裹了一层 middleware.Logging() ,这样我们的所有请求都会先由 Logging 方法过一遍。刚好这由跟上面所提到的 一个程序分配多个路由 可以进行一个很好的融合,可以通过为每个局部变量表都添加独有的中间件。

但是在实现上,其实这个方法(middleware.Logging),他也只是接受了一个 http.Handler 类型的参数并返回了一个 http.Handler ,这并没有发生什么魔法,就只是多用方法处理了一步而已。可以看一看详细的实现:

 // Logging 是一个中间件函数,用于记录每个请求的信息。
 // 它接收一个 http.Handler 作为参数,并返回一个新的 http.Handler。
 func Logging(next http.Handler) http.Handler {
  // http.HandlerFunc 是一个适配器,允许普通的函数作为 http.Handler 使用。
  // 我们返回的这个函数就是实际处理请求的中间件处理器。
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  // 1. 在请求到达核心业务逻辑之前执行
  startTime := time.Now()
  log.Printf("--> %s %s", r.Method, r.URL.Path)
 ​
  // 2. 调用下一个处理器 (next.ServeHTTP)
  // 这可能是另一个中间件,也可能是我们最终的 mux。
  // 请求会在这里被"暂停",直到核心业务逻辑处理完成。
  next.ServeHTTP(w, r)
 ​
  // 3. 在核心业务逻辑处理完成之后执行
  duration := time.Since(startTime)
  log.Printf("<-- %s %s (%v)", r.Method, r.URL.Path, duration)
  })
 }

在方法中,我们并没有 new http.Handler ,但是的确成功的返回了 Handler 对象,为什么呢,这跟 Go 的接口集成特性有关,在 Go 中只要我们实现了 接口 的所有方法,我们就已经继承了这个接口,而 http.Handler 中之后一个方法:

 type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
 }

认真看看,这跟 java 中的 AOP 非常像。在实现中的代码也有next.ServeHTTP(w,r) ,就好像是一个环绕通知。这其实也是一个洋葱模型。同时像 middleware.Logging(mux) ,外面可以在包任意层的处理,也是一层层的包洋葱皮(即前面的先处理,后面的后处理)

 请求 -> ListenAndServe -> [Logging Middleware Handler] -> [Mux Handler] -> [UsersHandler]
  | | |
  '-> (开始计时, 打印 "-->") '-> (匹配 /users) '-> (业务逻辑)
  | | |
 响应 <- ListenAndServe <- [Logging Middleware Handler] <- [Mux Handler] <- [UsersHandler]
  | |
  '-> (打印 "<--", 计算耗时) '-> (返回)