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 的方式设置的路由是全局路由表,使用局部路由表有更多的好处:
- 避免全局状态 (No Global State):
- 全局变量(默认 ServeMux 就是一个全局变量)在大型应用中往往会导致不可预测的行为和难以调试的问题。
- 自定义 mux 避免了这种全局状态,让你的路由配置更加独立和可控。
- 更好的封装和测试 (Better Encapsulation & Testability):
- 你可以为应用的不同部分创建不同的 ServeMux 实例,实现路由的模块化。
- 在测试时,你可以很容易地创建一个 ServeMux 实例,注册你想要测试的 Handler,然后对它进行独立的测试,而不会受到其他路由的影响。
- 中间件的基础 (Foundation for Middleware):
- 尽管我们还没有讲到中间件,但自定义 ServeMux 是实现中间件(在请求到达你的业务逻辑之前或之后执行一些通用操作,如日志、认证)的强大基础。你可以将中间件包装在你的 mux 外部。
- 清晰的应用架构 (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]
| |
'-> (打印 "<--", 计算耗时) '-> (返回)
Comments NOTHING