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

前言

首先写这系列文章,是为了记录我自己学习 Gin 框架的过程。 我并没有打算一开始就搭建一个成熟的项目骨架,毕竟对整个框架还不是特别熟悉,也没有用什么高级插件或第三方库。就是从最基础的内容开始,一步步去实现功能,比如路由怎么写、JSON 怎么接、参数怎么校验、数据库怎么操作等等,边学边练,慢慢摸索。 如果文章中有理解不准确的地方,欢迎各位大佬指出,我会持续更新、不断改进 ~ 同时也希望这些内容能帮到和我一样正在入门 Go 或 Gin 框架的朋友们,作为一份更贴近实战、按模块推进的学习参考

1. 项目初始化

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

bash 复制代码
mkdir gin-learn-notes

cd gin-learn-notes

go mod init gin-learn-notes

关于 go mod init gin-learn-notes

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

csharp 复制代码
go mod init gin-learn-notes

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

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

模块名的作用

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

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

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

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

go 复制代码
require github.com/gin-gonic/gin v1.10.0

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

创建入口文件 main.go

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

先写一个最简单的版本:

go 复制代码
// 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 复制代码
go run main.go

浏览器访问:

bash 复制代码
http://localhost:8080/ping

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

json 复制代码
{
  "message": "pong"
}

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

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

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

go 复制代码
r := gin.Default()

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

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

举个例子

go 复制代码
// 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,这样做的好处是:

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

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

路由拆分结构如下:

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

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

router/router.go

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

修改前的样子:

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

修改后的样子:

go 复制代码
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 复制代码
go run main.go

然后访问:

http://localhost:8080/ping

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

从内联逻辑到控制器分离

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

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

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

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

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

拆分之后的结构:

go 复制代码
// router/router.go
r.GET("/ping", controller.Ping)
go 复制代码
// 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,内容如下:

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,添加以下路由注册代码:

go 复制代码
// router/router.go

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

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

bash 复制代码
go run main.go

在浏览器或 Postman 中访问:

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

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

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

go 复制代码
func(c *gin.Context)

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

go 复制代码
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 响应;
  • 控制器结构清晰,让每个接口的逻辑单独维护,项目更易扩展

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

go 复制代码
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 参数并实现参数绑定。

相关推荐
我是前端小学生1 小时前
面试官:在go语言中,主协程如何等待其余协程完毕再操作?
go
关山月2 小时前
学习Go语言:循环和条件语句
go
我是前端小学生2 小时前
面试官:在go语言中,使用for select时,如果通道已经关闭会怎么样?如果只有一个case呢?
go
我是前端小学生2 小时前
面试官:在go语言中,在 for range 循环中对切片(slice)使用append操作,会造成无限循环吗?
go
三块钱07949 小时前
【原创】通过S3接口将海量文件索引导入elasticsearch
大数据·elasticsearch·搜索引擎·go
一个热爱生活的普通人19 小时前
JWT认证:在gin服务中构建安全的API接口
后端·go·gin
洛卡卡了21 小时前
Gin 框架学习实录 · 第4篇:参数校验、结构体拆分与控制器职责解耦
go
洛卡卡了21 小时前
Gin 框架学习实录 · 第3篇:集成 GORM + MySQL,实现注册用户入库
go
Pandaconda1 天前
【新人系列】Golang 入门(七):闭包详解
开发语言·经验分享·笔记·后端·golang·go·闭包
forever231 天前
kubebuilder创建k8s operator项目(下)
go