Gin 框架学习实录 · 第1篇:项目初始化、路由拆分与控制器结构设计

466 阅读9分钟

前言

写这系列文章,是为了记录刚开始自己学习 Gin 框架的过程。刚开始并没有打算一开始就搭建一个成熟的项目骨架,毕竟对整个框架还不是特别熟悉,也没有用什么高级插件或第三方库。就是从最基础的内容开始,一步步去实现功能,比如路由怎么写、JSON 怎么接、参数怎么校验、数据库怎么操作等等,边学边练,慢慢摸索。

1. 项目初始化

首先第一步就是新建一个 Go 项目,初始化 mod 模块:

mkdir gin-learn-notes

cd gin-learn-notes

go mod init gin-learn-notes

关于 go mod init gin-learn-notes

我们使用 Go Module 来管理项目依赖,因此在项目初始化时,需要先执行:

go mod init gin-learn-notes

这条命令会创建一个 go.mod 文件,告诉 Go 编译器:

  • 当前这个项目的模块名是 gin-learn-notes
  • 未来在这个模块下安装的依赖、引入的包,都会基于这个路径来进行管理

模块名的作用

比如我们后面会在代码中引用:

import "gin-learn-notes/controller"

这就依赖于我们 go.mod 里定义的模块名一致,否则导包会出错。

这一步做了什么?

  • 创建了 go.mod 文件,记录模块名与依赖;
  • 之后我们使用 go get 安装 Gin 或其他库时,依赖都会记录在这个文件中,便于管理。

然后这里我也是大概了解了Go Modules 的一些小知识:

Go Modules 常用命令整理

命令作用说明
go mod init初始化项目模块,生成 go.mod 文件
go mod tidy清理无用依赖,同时自动添加缺失的依赖
go mod download下载 go.mod 中声明的依赖到本地缓存
go mod verify校验依赖包是否被修改或篡改,确保安全性
go mod why查看当前项目为什么会依赖某个包(依赖链分析)

安装 Gin 框架:

go get -u github.com/gin-gonic/gin
  • -u 表示更新依赖(首次安装也适用)
  • 安装完成后会在 go.mod 里自动添加依赖
  • go.sum 文件也会生成,用来校验依赖的完整性

这时 go.mod 会自动添加 Gin 的依赖,go.sum 文件也会自动生成用于版本校验。

安装完成后,go.mod 文件中会多出类似这样的内容:

require github.com/gin-gonic/gin v1.10.0

如果我们不确定当前安装了哪些依赖,也可以查看 go.sum 文件,它记录了每个依赖包的版本和校验信息。

创建入口文件 main.go

项目入口文件是 main.go,用来启动 Gin 引擎和运行服务。

先写一个最简单的版本:

// main.go
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()      // 初始化 Gin 引擎,带 Logger + Recovery 中间件
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run(":8080")          // 启动服务,监听 8080 端口
}

每行代码说明:

代码行含义解释
gin.Default()返回一个带 Logger 和 Recovery 的默认路由引擎
r.GET(...)定义一个 GET 路由,访问 /ping 时返回 JSON
c.JSON(...)返回一个 JSON 响应
r.Run(":8080")启动服务,监听本地 8080 端口

启动服务测试

go run main.go

image.png

浏览器访问:

http://localhost:8080/ping

我们会看到返回的 JSON 响应:

{
  "message": "pong"
}

image.png

这就代表我们的 Gin 项目已经成功跑起来了! 这也是我们初次学习Gin框架的第一步。

gin.Default() 和 gin.New() 有什么区别?

我在初始化 Gin 的时候,遇到的第一个疑问就是:

r := gin.Default()

那它和 gin.New() 有什么区别呢?后来我查了文档和源码,又问了下AI,大概理解如下 👇

方法含义自动加载的中间件适合场景
gin.Default()快速启动✅ Logger(日志) ✅ Recovery(异常捕获)开发环境,快速启动调试
gin.New()空壳引擎❌ 不加载任何中间件生产环境 / 自定义中间件

举个例子

// Default() 自动注册中间件(推荐开发时使用)
r := gin.Default()

// New() 是最“干净”的引擎,需要自己添加中间件
r := gin.New()
r.Use(gin.Logger(), gin.Recovery()) // 手动注册中间件

总结理解

  • 如果是刚开始练手,建议用 gin.Default(),它更“开箱即用”
  • 如果打算完全控制中间件、日志格式、安全策略等,gin.New() 更灵活

因为我刚学,所以个人目前用的是 gin.Default(),等后期熟悉后,会尝试自己封装中间件并切换为 gin.New() 自定义配置。

路由拆分

main.go 是程序的启动入口,它应该只负责初始化引擎、启动服务,而不应该承担“注册所有接口”的职责。否则随着接口增多,main.go 会变得非常臃肿,不利于维护和扩展。

所以我们可以把路由注册逻辑单独放在 router/router.go,这样做的好处是:

  • 遵守单一职责原则,结构清晰
  • 后面如果接口很多,可以按模块拆成多个路由文件

虽然这只是个小项目,但从一开始就保持良好的结构习惯,对我们后续开发非常有帮助。

路由拆分结构如下:

gin-learn-notes/
├── main.go               // 启动入口,只负责引擎初始化
├── router/
│   └── router.go         // 路由注册逻辑集中处理

我们在router/router.go里面用一个函数返回 *gin.Engine,让 main.go 调用它

router/router.go

package router

import (
	"github.com/gin-gonic/gin"
)

func InitRouter() *gin.Engine {
	// 创建默认的 Gin 引擎
	r := gin.Default()

	// 定义一个基本的路由
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

	return r
}

修改 main.go

修改前的样子:

func main() {
	r := gin.Default()
	r.GET("/ping", ...) // 👈 现在我们要把这一块拿出去
	r.Run(":8080")
}

修改后的样子:

package main

import (
    "gin-learn-notes/router" // ⬅️ 导入我们自己的路由包
)

func main() {
	r := router.InitRouter() // ⬅️ 使用封装好的函数
	r.Run(":8080")
}

⚠️ 注意:"go-learn-notes/router" 中的 go-learn-notes 是我们 go mod init 时填的模块名,确保一致。

这样,项目结构就开始变得“分工明确”了:

  • main.go 专注启动
  • router.go 专注路由注册

后面我们还可以继续拆出控制器、服务层、数据库模型等等,形成完整的分层架构。

我们改完后在终端中执行:

go run main.go

然后访问:

http://localhost:8080/ping

如果能看到 {"message": "pong"},说明我们拆分路由没啥问题。

从内联逻辑到控制器分离

在我们之前的测试路由中,我们是直接这样写的:

r.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
})

虽然写起来很方便,但这个处理函数是直接写在路由注册里的,随着项目逐渐复杂,每个接口都有自己的业务逻辑,如果都塞在路由里,维护起来非常困难。

路由的职责应该只是“分发请求”,而不是处理具体的业务逻辑。

所以,我们继续遵循单一职责原则,把处理逻辑从路由中抽离,放到 controller/ 目录中的控制器文件中。

拆分之后的结构:

// router/router.go
r.GET("/ping", controller.Ping)
// controller/ping.go
func Ping(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

这样做的好处是:

  • 路由只负责定义“谁负责处理”,代码更清晰
  • 业务逻辑集中管理,方便测试和维护
  • 日后可以继续往下拆分(service 层、model 层),形成 MVC 分层结构

后面我们还会进一步把控制器逻辑抽出 service 层、数据库抽出 model 层,逐步形成可维护的项目架构。

Gin 接收参数 + 返回 JSON 示例

我们刚刚把处理逻辑从路由中拆到了控制器里。接下来,我们可以通过一个简单的接口,来了解一下 Gin 如何接收参数并输出 JSON。

第一步:编写控制器 hello.go

controller 目录下创建一个新的文件 hello.go,内容如下:

// controller/hello.go
package controller

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

// HelloHandler 处理 /hello 路由
func HelloHandler(c *gin.Context) {
    // 获取 name 参数,默认为 "World"
    name := c.DefaultQuery("name", "World")

    // 返回 JSON 响应
    c.JSON(http.StatusOK, gin.H{
        "message": "Hello, " + name + "!",
    })
}

这里我们使用了 c.DefaultQuery() 方法,从 URL 中获取 name 参数,如果没有传入,则使用默认值 "World"。

第二步:在路由中注册 hello 接口

打开 router/router.go,添加以下路由注册代码:

// router/router.go

r.GET("/hello", controller.HelloHandler) // 支持访问 /hello?name=Gin

第三步:运行项目进行测试

go run main.go

在浏览器或 Postman 中访问:

  • http://localhost:8080/hello
    返回:{"message": "Hello, World!"}
  • http://localhost:8080/hello?name=Gin
    返回:{"message": "Hello, Gin!"}

思考:为什么路由函数里总是用 c *gin.Context

我们在上面的代码中会发现无论我们把处理逻辑直接写在路由里,还是拆分到控制器函数中,处理函数的签名都是这样的:

func(c *gin.Context)

这个就是Gin 框架的设计核心 —— Context 对象(全名:*gin.Context 。 Gin 所有处理器函数的参数都是:

func(c *gin.Context)

它是 Gin 最重要的对象,包含:

方法作用
c.Param()路径参数
c.Query()查询参数
c.ShouldBindJSON()绑定 JSON
c.JSON()返回 JSON
c.Set() / c.Get()设置和获取上下文变量(常用于中间件)

返回响应的常用方式:

方法说明
c.JSON(200, data)返回 JSON 数据
c.String(200, "text")返回纯文本响应
c.HTML(200, "tpl.html", data)渲染 HTML 模板
c.File("file.zip")直接下载文件

后面我们还会经常通过 c 对象来获取请求头、设置响应头、操作 Cookies、获取 token、设置上下文用户信息等,可以说 Context 是 Gin 应用的“灵魂级对象”。

小结

  • 使用 c.DefaultQuery() 可以方便地从 URL 查询字符串中获取参数并设置默认值;
  • 使用 c.JSON() 可以快速返回 JSON 响应;
  • 控制器结构清晰,让每个接口的逻辑单独维护,项目更易扩展

现在我们的项目结构像这样:

gin-learn-notes/
├── controller/
│   └── hello.go
|   └── index.go
├── router/
│   └── router.go
├── main.go
├── go.mod

最后

在本篇中,我们主要完成了以下内容:

  • 初始化 Gin 项目,理解了 go mod initgo get 的作用
  • 编写了项目入口 main.go,并学习了 gin.Default()gin.New() 的区别
  • 介绍了项目结构的基本划分,遵循单一职责原则
  • 将路由从 main.go 拆分到 router/router.go,实现结构解耦
  • 将逻辑从路由中抽出,创建了控制器 hello.go,实现了参数接收与 JSON 输出

通过这些步骤,我们为后续的开发打下了清晰、规范的基础。Gin 项目的结构感,正在逐渐建立起来。

本篇对应代码提交记录

commit: 2146589efa7fb67fd60a64c5bc0e2cf4177363f0

👉 GitHub 源码地址:github.com/luokakale-k…


下一篇我们将继续深入学习:如何通过 POST 接口接收 JSON 参数并实现参数绑定。