当你在 Go 中构建一个 Web 应用程序时,使用 Gin 框架、Gorm ORM、Viper 配置库、Zap 日志库和 Redis 数据库可以让你高效地开发功能丰富的应用程序。
1、项目初始化
我为里通过Goland创建一个go 项目
位置:就是你项目保存在本地的路径
GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。
环境:一般是直接访问国内地址,我上面这是国内七牛云的 GOPROXY=https://goproxy.cn,direct
1.1 安装依赖
go get github.com/redis/go-redis/v8 //v8 对应Redis6
go get github.com/spf13/viper
go get go.uber.org/zap
go get gorm.io/driver/mysql
go get gorm.io/gorm
go get github.com/gin-gonic/gin
1.2 配置文件管理
在 webapp 下创建一个 config.yaml 配置文件,示例如下:
port: "8080"
log:
level: "debug"
filename: "日志文件名字.log"
max_size: 200
max_age: 30
max_backups: 7
mysql:
host: "你的ip地址"
port: 3306
username: "你的用户名"
password: "你的密码"
dbname: "你的数据库名"
redis:
host: "你的ip地址"
port: 6379
db: 7
password: "你的密码"
pool_size: 100
1.3 初始化viper
是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。
在webapp下config目录,然后创建一个settings.go文件,示例如下:
package config
import (
"fmt"
"github.com/spf13/viper"
)
var Conf = new(AppConfig)
type AppConfig struct {
Port string `mapstructure:"port"`
*LogConfig `mapstructure:"log"`
*MySQLConfig `mapstructure:"mysql"`
*RedisConfig `mapstructure:"redis"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}
type MySQLConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
}
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
DB int `mapstructture:"db"`
Password string `mapstructure:"password"`
PoolSize int `mapstructure:"pool_size"`
}
// Init 结构体形式
func Init() (err error) {
viper.SetConfigName("config") //指定配置文件名称(不需要带后缀)
viper.SetConfigType("yaml") //指定配置文件类型(专用于从远程获取配置信息时,指定配置)
viper.AddConfigPath("./settings") //指定配置文件路径(这里是相对路径)
err = viper.ReadInConfig()
if err != nil {
fmt.Printf("viper.ReadInConfig() failed,err:%v\n", err)
return
}
//把读取到的配置信息反序列化到Conf变量中
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed,err:%v\n", err)
}
return
}
1.4 初始化zap日志
zap是非常快的、结构化的,分日志级别的Go日志库
在config目录下创建logger.go文件,示例如下
package config
import (
"fmt"
"github.com/spf13/viper"
)
var Conf = new(AppConfig)
type AppConfig struct {
Port string `mapstructure:"port"`
*LogConfig `mapstructure:"log"`
*MySQLConfig `mapstructure:"mysql"`
*RedisConfig `mapstructure:"redis"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Filename string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}
type MySQLConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
}
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
DB int `mapstructture:"db"`
Password string `mapstructure:"password"`
PoolSize int `mapstructure:"pool_size"`
}
// Init 结构体形式
func Init() (err error) {
viper.SetConfigName("config") //指定配置文件名称(不需要带后缀)
viper.SetConfigType("yaml") //指定配置文件类型(专用于从远程获取配置信息时,指定配置)
viper.AddConfigPath(".") //指定配置文件路径(这里是相对路径)
err = viper.ReadInConfig()
if err != nil {
fmt.Printf("viper.ReadInConfig() failed,err:%v\n", err)
return
}
//把读取到的配置信息反序列化到Conf变量中
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed,err:%v\n", err)
}
return
}
1.5 初始化Gorm
在webapp下创建一个dao目录再创建mysql目录,然后创建一个mysql.go文件,示例如下:
package mysql
func Init() (db *gorm.DB, err error) {
cfg := settings.Conf.MySQLConfig
fmt.Printf("%v", cfg.Dbname)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Dbname,
)
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
zap.L().Error("connect DB failed,err:%v\n", zap.Error(err))
return
}
return
}
1.6 初始化Redis
在webapp下创建一个dao目录再创建redis目录,然后创建一个redis.go文件,示例如下
package redis
var rdb *redis.Client
func Init(cfg *settings.RedisConfig) (err error) {
ctx := context.Background()
rdb = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d",
cfg.Host, cfg.Port,
),
Password: cfg.Password,
DB: cfg.DB,
PoolSize: cfg.PoolSize,
})
_, err = rdb.Ping(ctx).Result()
return
}
1.7 初始化路由
在webapp下创建routes路由,然后创建routes.go文件,示例如下
package routes
func Setup(h *controller.Handler) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(logger.GinLogger(), logger.GinRecovery(true))
r := gin.Default()
r.Static("/static", "../static") //静态文件
r.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
r.POST("/ddd/user/register/", h.UserHandler.Register)
}
1.8 main函数
在webapp下,创建main.go文件,我们需要初始化Viper配置管理库,初始化日志,初始化Handler(构建依赖),注册路由,示例如下 :
package main
import (
"fmt"
"go.uber.org/zap"
"go_web/config"
"go_web/routes"
)
// go web 开发通用的脚手架模板
func main() {
initServer()
}
func initServer() {
//1、加载配置
if err := config.Init(); err != nil {
fmt.Printf("init settings failed,err:%v\n", err)
return
}
zap.L().Debug("settings init success...")
//2、初始化日志
if err := config.Init(config.Conf.LogConfig); err != nil {
fmt.Printf("init logger failed,err:%v\n", err)
return
}
defer zap.L().Sync()
zap.L().Debug("logger init success...")
//3、初始化Handler
handler, err := controller.BuildInjector()
if err != nil {
panic(err)
}
//4、注册路由
r := routes.Setup(handler)
fmt.Println("listen port:", config.Conf.Port)
r.Run(fmt.Sprintf(":%s", config.Conf.Port))
}
2、具体实践
2.1 Controller层handler
在这里把所有依赖导入,这里也可以使用Wire框架,实现依赖注入。在参考文章有提供
package controller
import (
"go_web/dao/mysql"
"go_web/service/impl"
)
type Handler struct {
UserHandler *UserHandler
}
func NewHandler(userHandler *UserHandler) *Handler {
return &Handler{
UserHandler: userHandler,
}
}
// BuildInjector 导入所有依赖
func BuildInjector() (*Handler, error) {
db, err := mysql.Init()
if err != nil {
return nil, err
}
//dao
userDao := mysql.NewUserDao(db)
//service
userService := impl.NewUserService(userDao)
//handler
userHandler := NewUserHandler(userService)
//处理层
handler := NewHandler(userHandler)
return handler, nil
}
2.2 UserHanler
处理层定义一个或多个Service,对外提供一个函数,实现依赖注入。其他层也一样
type UserHandler struct {
UserService *impl.UserService
}
func NewUserHandler(userService *impl.UserService) *UserHandler {
return &UserHandler{UserService: userService}
}
func (h *UserHandler) Register(ctx *gin.Context) {
h.UserService.Register()
}
2.3 Service层
如下, 我只定义了一个接口,然后通过impl包下userServiceImpl.go实现接口。还有就是我会在userService下定义一个结构体,对应MySQL表的结构体不同,返回一些前端需要的参数,即字段相对于mysql表下的字段,可能增加或减少。
userService
type userService interface {
Register(name, password string) (err error)
}
impl/userServiceImpl
type UserService struct {
UserDao *mysql.UserDao
}
func NewUserService(userDao *mysql.UserDao) *UserService {
return &UserService{UserDao: userDao}
}
func (srv *UserService) Register(name, password string) (id int64, err error) {
newUser := &mysql.User{Name: name, Password: EnCoder(password)}
return srv.UserDao.InsertUser(newUser)
}
也可以通过gorm的Transaction函数实现事务,保证数据的一致性。如下代码:
2.4 Dao 层
Dao层,最好保证方法的单一性,即一个方法实现一个功能。复杂的逻辑还是在业务层实现吧
func NewUserDao(db *gorm.DB) *UserDao {
return &UserDao{DB: db}
}
type UserDao struct {
DB *gorm.DB
}
type User struct {
ID int64 `json:"id"`
Name string
Password stirng
}
func (dal *UserDao) InsertUser(user *User) (id int64, err error) {
if err = dal.DB.Create(&user).Error; err != nil {
zap.L().Error("UserDao.InsertUser failed", zap.Error(err))
return 0, ErrorInsert
}
return int64(user.ID), nil
}
3、项目结构图
参考
www.liwenzhou.com/posts/Go/za…