小蓝书后端项目搭建(一)

本项目所有代码均已上传githubgithub地址为:https://github.com/M-500/small-yello-book

大家好,我是一根甜苦瓜。

今天分享怎么搭建小蓝书项目的后端(单体应用),这里我们采用Golang语言,Gin框架,MySQL数据库,Redis缓存等技术栈,下面我们来一步一步的搭建。

今天的目标:

  1. 搭建项目目录结构
  2. 集成配置文件的读取
  3. 对目录结构进行拆分(MVC)
  4. 实现一个简单的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":"火鸡味锅巴"}                   

至此,一个简单的基于 ginweb 服务就搭建好了。

2. 集成Viper

上面的代码中,我们的端口号是写死的,这样不太好,我们可以通过配置文件来读取端口号,进一步思考,项目中很多地方会用到配置文件,比如:数据库的连接参数,redis的连接参数等等,这时候我们就需要一个配置文件读取的工具,这里我们选择 Viper

Viper 是一个适用于 Go 应用程序的完整配置解决方案,它可以处理多种配置需求和格式,支持环境变量,jsonyamltoml等等。

首先,我们需要安装 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

当然上面的代码还有一些不合理的地方,比如:

  1. 配置文件的路径是写死的,不太好,我们可以通过环境变量来指定配置文件的路径
  2. 配置文件的读取和映射是在 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层等)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一根甜苦瓜

祝老板身体棒棒,夜夜笙歌

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值