本项目所有代码均已上传
github,github地址为:https://github.com/M-500/small-yello-book
大家好,我是一根甜苦瓜。
今天分享怎么搭建小蓝书项目的后端(单体应用),这里我们采用Golang语言,Gin框架,MySQL数据库,Redis缓存等技术栈,下面我们来一步一步的搭建。
今天的目标:
- 搭建项目目录结构
- 集成配置文件的读取
- 对目录结构进行拆分(
MVC) - 实现一个简单的
HTTP接口
1. 创建项目
在创建项目时,我们先要考虑好项目的目录结构,不然打开项目一堆文件夹平铺,会让人很头疼。这里最终单体项目的目录结构大致如下:
.
├── docs # 保存swagger文档相关
├── etc # 项目的配置文件
│ ├── prod.yaml
│ └── test.yaml
├── internal
│ ├── app.go
│ ├── conf # 保存配置文件的读取和映射关系
│ ├── initialize # 项目的初始化相关依赖
│ ├── models # 数据库模型类
│ ├── repo # 抽象数据的存储层
│ ├── service # 业务逻辑层
│ ├── types # 项目的公共类型 比如form表单对应的结构体,返回的json数据对应的结构体
│ └── web # 项目的web接口
├── logs # 保存日志文件
├── main.go # 项目的入口文件
├── pkg # 项目的公共包
└── static # 项目的静态文件
当然,我们一步步来,先创建一个项目,然后再来慢慢的按照上面的目录结构来完善。
cd 你的工作目录
# 创建项目
mkdir gin-svc && cd gin-svc
# 初始化go mod
go mod init gin-svc
# 创建main.go
touch main.go
此时,项目的目录结构如下:
gin-svc
├── go.mod
└── main.go
OK,接下来就在main.go中写入以下代码,使用gin框架实现一个简单的web服务
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
engine.GET("/hello", HelloCtl)
engine.Run(":10086")
}
func HelloCtl(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "火鸡味锅巴",
})
}
接下来,我们就可以运行项目了
# 同步相关依赖
go mod tidy
# 运行项目
go run main.go
之后浏览器访问 127.0.0.1:10086/hello 可以看到返回的内容。这里我采用 curl http://127.0.0.1:10086/hello 来测试。
% curl http://127.0.0.1:10086/hello
{"message":"火鸡味锅巴"}
至此,一个简单的基于 gin 的 web 服务就搭建好了。
2. 集成Viper
上面的代码中,我们的端口号是写死的,这样不太好,我们可以通过配置文件来读取端口号,进一步思考,项目中很多地方会用到配置文件,比如:数据库的连接参数,redis的连接参数等等,这时候我们就需要一个配置文件读取的工具,这里我们选择 Viper。
Viper 是一个适用于 Go 应用程序的完整配置解决方案,它可以处理多种配置需求和格式,支持环境变量,json,yaml,toml等等。
首先,我们需要安装 Viper,使用命令 go get github.com/spf13/viper 安装。
go get github.com/spf13/viper
然后创建一个配置文件 etc/dev.yaml,内容如下:
server:
port: 10086
接下来,我们就可以在项目中使用 Viper 来读取配置文件了。通常会用一个结构体来和yaml中的配置文件进行映射,这样方便我们在项目中使用。先来创建这个结构体
package conf
type ConfigInstance struct {
Server ServerCfg `mapstructure:"server"`
}
type ServerCfg struct {
Port int `mapstructure:"port"`
}
然后在 main.go 中使用 Viper 读取配置文件并映射到结构体中。
package main
import (
"fmt"
"gin-svc/internal/conf"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
func main() {
engine := gin.Default()
engine.GET("/hello", HelloCtl)
var appCfg conf.ConfigInstance // 1. 定义配置结构体
configPath := "etc/dev.yaml" // 2. 配置文件路径
viper.SetConfigFile(configPath)
err := viper.ReadInConfig() // 3. 读取配置文件
if err != nil {
panic(err)
}
err = viper.Unmarshal(&appCfg) // 4. 解析配置文件和结构体的映射
if err != nil {
panic(err)
}
addr := fmt.Sprintf(":%d", appCfg.Server.Port) // 5. 从配置文件中获取端口
engine.Run(addr)
}
func HelloCtl(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "火鸡味锅巴",
})
}
接下来,我们就可以运行项目了,看看是否能够读取到配置文件中的端口号。
go mod tidy
go run main.go
当然上面的代码还有一些不合理的地方,比如:
- 配置文件的路径是写死的,不太好,我们可以通过环境变量来指定配置文件的路径
- 配置文件的读取和映射是在
main函数中,不太好,我们可以将这部分逻辑封装到一个函数中
2.1 命令行参数传递路径
我们可以通过命令行参数来传递配置文件的路径,这样一来可以通过命令行参数的不同来制定不同的配置文件,比如再创建一个etc/prod.yaml配置文件,用来代表生产环境的配置,内容如下:
server:
port: 8081
这里我们使用 flag 包来实现读取命令行参数,修改 main.go 如下:
package main
import (
"flag"
"fmt"
"gin-svc/internal/conf"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
var configFile = flag.String("config", "etc/dev.yaml", "配置文件路径")
func main() {
flag.Parse() // 读取命令行参数
engine := gin.Default()
engine.GET("/hello", HelloCtl)
var appCfg conf.ConfigInstance // 1. 定义配置结构体
//configPath := "etc/dev.yaml" // 2. 配置文件路径
viper.SetConfigFile(*configFile)
err := viper.ReadInConfig() // 3. 读取配置文件
if err != nil {
panic(err)
}
err = viper.Unmarshal(&appCfg) // 4. 解析配置文件和结构体的映射
if err != nil {
panic(err)
}
addr := fmt.Sprintf(":%d", appCfg.Server.Port) // 5. 从配置文件中获取端口
engine.Run(addr)
}
func HelloCtl(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "火鸡味锅巴",
})
}
那么在运行项目时,我们可以通过命令行参数来指定配置文件的路径,比如:
go run main.go -config=etc/prod.yaml
如果不传递参数,默认读取 etc/dev.yaml 配置文件。
2.2 封装配置文件读取
上面的代码中,配置文件的读取和映射是在 main 函数中,不太好,我们可以将这部分逻辑封装到一个函数中,这样可以提高代码的可读性。由于配置文件的解析在每个项目中都会用到,而且都不同,所以应该封到项目内部(internal包中)。
又因为,项目的配置文件的读取和映射属于项目的初始化工作,所以我们可以将这部分逻辑放到 internal/initialize 包中。创建文件internal/initialize/cfg.go写入下面的内容
package initialize
import (
"gin-svc/internal/conf"
"github.com/spf13/viper"
)
func SetupConfig(filePath string) (*conf.ConfigInstance, error) {
var appCfg conf.ConfigInstance
viper.SetConfigFile(filePath)
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
err = viper.Unmarshal(&appCfg)
if err != nil {
return nil, err
}
return &appCfg, nil
}
func MustSetupConfig(filePath string) *conf.ConfigInstance {
cfg, err := SetupConfig(filePath)
if err != nil {
panic(err)
}
return cfg
}
关于初始化配置文件失败是抛出error,还是说直接panic,这个根据实际情况来定,如果配置文件是必须的,那么直接panic,如果不是必须的,那么抛出error。所以提供了两个函数,一个是
SetupConfig,一个是MustSetupConfig。
接下来main.go中的内容就简单了很多,如下:
package main
import (
"flag"
"fmt"
"gin-svc/internal/initialize"
"github.com/gin-gonic/gin"
)
var configFile = flag.String("config", "etc/dev.yaml", "配置文件路径")
func main() {
flag.Parse() // 读取命令行参数
engine := gin.Default()
engine.GET("/hello", HelloCtl)
appCfg := initialize.MustSetupConfig(*configFile) // 1. 调用读取配置文件的函数
addr := fmt.Sprintf(":%d", appCfg.Server.Port) // 2. 从配置文件中获取端口
engine.Run(addr)
}
func HelloCtl(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "火鸡味锅巴",
})
}

至此,项目的目录结构如下:
.
├── etc
│ ├── dev.yaml
│ └── prod.yaml
├── go.mod
├── go.sum
├── internal
│ ├── conf
│ │ └── config.go # 配置文件的映射关系的结构体
│ └── initialize
│ └── cfg.go # 初始化配置文件的逻辑
└── main.go
3. gin框架的路由拆分
按照上面说的入口文件代码尽可能简单干净,我们可以将路由的注册逻辑拆分到其他的包中,这里我们将路由的注册逻辑拆分到 internal/web 包中。创建文件internal/web/controller/hello.go,写入下面的内容(把HelloCtl函数挪过来):
package controller
import "github.com/gin-gonic/gin"
func HelloCtl(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"message": "火鸡味锅巴",
})
}
接下来,我们创建一个路由注册的文件internal/web/router.go,(后续的路由都在这里进行集中管理注册)写入下面的内容:
package web
import (
"gin-svc/internal/web/controller"
"github.com/gin-gonic/gin"
)
// RegisterRouters is a function.
func RegisterRouters(engine *gin.Engine) {
engine.GET("/hello", controller.HelloCtl)
}
最后main函数便可以进一步简化,如下:
package main
import (
"flag"
"fmt"
"gin-svc/internal/initialize"
"gin-svc/internal/web"
"github.com/gin-gonic/gin"
)
var configFile = flag.String("config", "etc/dev.yaml", "配置文件路径")
func main() {
flag.Parse() // 读取命令行参数
engine := gin.Default() // 创建gin引擎
appCfg := initialize.MustSetupConfig(*configFile) // 1. 读取配置文件
addr := fmt.Sprintf(":%d", appCfg.Server.Port) // 2. 从配置文件中获取端口
web.RegisterRouters(engine) // 3. 注册路由
engine.Run(addr)
}
是不是整体代码结构就清晰很多,此时的目录结构如下:
.
├── etc # 保存配置文件
│ ├── dev.yaml
│ └── prod.yaml
├── go.mod
├── go.sum
├── internal
│ ├── conf # 保存配置文件对应的结构体
│ │ └── config.go
│ ├── initialize # 存放初始化的逻辑,包括配置文件,数据库,redis等等等
│ │ └── cfg.go
│ └── web # 存放web相关的逻辑 包括 controller,router,中间件等等等
│ ├── controller
│ │ └── hello.go
│ └── routers.go
└── main.go
当然最后还是要验证拆分后的项目是否可以正常运行
go mod tidy
go run main.go
用curl测试一下
% curl http://127.0.0.1:10086/hello
{"message":"火鸡味锅巴"}%
总结
本文主要介绨了如何使用Gin框架搭建一个简单的web服务,以及如何使用Viper来读取配置文件,最后将路由的注册逻辑拆分到其他的包中,使得项目的代码结构更加清晰(代码逻辑其实很简单)
预告: 下一讲会通过用户注册这个功能作为切入点,详细讲解如何集成MySQL数据库,以及如何对项目的目录结构进行拆分。(包括pkg包,service层,repo层,dao层等)

3万+

被折叠的 条评论
为什么被折叠?



