引言
GoLang语言开发,有beego、gin等框架,gin框架使用较多,笔者也较为熟悉。gin框架提供的是基础框架,对于如何封装框架gin框架没有做固定的要求,我们在go程序开发中对它进行的封装也有很多不同的方法,笔者看到的很多golang的gin框架封装代码的可以说千差万别、各自为阵,有些封装的也冗余太多,不够精简和统一,其实让我们更加清晰的看懂和进行加入团队进行开发的框架结构,决定了我们的开发效率,如果很多差异太多的框架封装,往往会占用我们很多学习成本,其实实现的功能都是类似的,但开发效率和学习成本却不同。这里比较推荐php的thinkphp6的框架封装,用tp6框架封装是通用的MVC框架,封装的结构也非常清晰易读,其实框架可以与编程语言无关,都使用通用的MVC结构框架,源码目录结构都可以统一,实现语言可以各自不同。目前比较流行的“微服务框架”,可以用不同语言来实现不同服务,基于业务或开发效率需要,我们可以根据情况,选择各自语言擅长特性开发出不同的服务接口,在应用中使用,不一定非要是同一门语言,比如php擅长web端并开发效率高、go擅长性能比较高效、高并发等特性,我们根据情况开发选择开发不同的微服务接口,即使php开发了并不太擅长的高性能接口,其实很多地方并不一定是都需要非常高性能,相差也就零点几秒的效率,很多业务都可以正常使用,如果到时某业务接口有性能瓶颈,需要性能优化时,再用go语言替换也不迟,这样我们可以先关注到业务流程的总体实现,先不用过渡关注是否是单一纯语言开发等技术问题,开发效率高地实现业务需求后,再做微服务接口的替换优化也不迟。很多大公司有资金实力,要么是纯语言,JAVA、golang来开发应用,投入了很大开发成本,以致于很多项目延期,资金不足或浪费,笔者认为是巨大浪费。客户真正需要业务快速实现,体现业务价值,之后再进行性能优化也不迟,微服务架构思想可以支持异构、多语言框架,如果改变一些思路,可能能够充分利用不同语言优势,取长补短,即能快速开发出有业务价值产品,也会带来很多不必要的成本浪费。
参考一些实际使用的应用框架如go-admin等,同时也借鉴TP6等框架,我们通过对gin框架基本用法的介绍,了解在不做封装时的开发调用,然后我们提供gin框架通过比较通用的MVC结构封装的架构,用于应用开发,能够比较清晰、统一,为开发框架提供指南和参考。
Gin框架
介绍
Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点;
对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错
借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范
参考
gin框架:https://github.com/gin-gonic/gin
go-admin框架:https://github.com/go-admin-team/go-admin.git
https://doc.zhangwj.com/guide/go-modules.html
ThinkPHP6.0完全开发手册:
https://www.kancloud.cn/manual/thinkphp6_0/1037495
建议:gin框架和php语言的laravel、tp6框架有些接近,我们可以按laravel、tp6框架思想来学习使用。
安装
按照安装第三方依赖包方式安装即可(前面讲过)
# go get -u github.com/gin-gonic/gin
注:在go的v16版本后推荐使用go install,go get方式不再使用,语法为:
go install <package>@<version>
我们先来看一下gin框架安装到哪里了。在安装go环境时,我们用“go env”命令可以查看到安装参数,注意“GOPATH”、“GOROOT”这2个参数变量的区别,如下:
GOROOT:是go基础环境的根目录,包含go的基础语法库。
GOPATH:是应用开发的根目录,是go的扩展安装依赖包的目录。
GOROOT最好和GOPATH设置为不同的目录,这样可以有所区分,GOROOT也可以替换,比如升级不同的go版本就可以替换不同版本目录,这样环境可以灵活适应。
按照上述的理论,gin框架作为第三方的依赖包安装目录就在“GOPATH”,我们看看这个目录下内容
主要为bin、pkg,pkg中就是安装的第三方包,再看截图
pkg/mod下是安装的所有依赖包,这些包里面都是封装的go语言源码,我们在应用开发时,直接引用这些第三方依赖包(比如gin框架包) ,我们就可以不用自己写框架,框架的作用就是大牛们写好一些通用的源码,应用开发中会经常用到,不用重复发明同样的“柱子”,用这些大牛写的基础框架包或者工具、组件包,可以省去不少开发时间。
上述“go get”或“go install”是通过指令安装第三方包,实际应用开发中,可以把安装包组织到配置文件中通过“go mod tidy”指令统一安装,配置文件为go.mod,类似于php的composer打包配置工具(配置文件为composer.json),我们看看其中内容,如下:
我们执行“go mod tidy”指令后,会自动按配置文件中设置的依赖包和版本下载相应的源码包,并将依赖包存放到设定的本地“GOPATH”的pkg/mod目录中,这样我们在后续的开发中方便的引用这些依赖包进行开发了。
gin框架基本用法
gin框架在不做封装时的开发调用,为了解框架运行机制,我们给出如下示例:
示例1:web方式访问接口
建立文件gin-frame/example.go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "ok",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
启动运行此example.go,作为服务监听
# go run example.go
浏览器访问:http://127.0.0.1:8080/index
示例2:数据库orm访问
需要先安装依赖包gorm
# go get -u gorm.io/gorm
本地创建数据库,名为db_go_shop,建立测试表,测试数据,如下
CREATE TABLE `tp_goods` (
`goods_id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(100) NOT NULL,
`shop_id` int(11) NOT NULL,
`category_id` int(5) DEFAULT NULL,
PRIMARY KEY (`goods_id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of tp_goods
-- ----------------------------
INSERT INTO `tp_goods` VALUES ('1', '花生', '1', '1');
INSERT INTO `tp_goods` VALUES ('2', '牛奶', '1', '1');
目录结构如下:
goods.go
package models
import "gorm.io/gorm"
type Goods struct {
gorm.Model
GoodsId int `gorm:"primarykey"` //对应表中的字段名 goods_id
ShopId int
CategoryId int
GoodsName string
}
models.go
package models
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
var (
dsn string = "root:root@tcp(localhost:3306)/db_go_shop?charset=utf8mb4"
)
func DB() *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "tp_",
SingularTable: true,
},
})
fmt.Println("err : ", err)
return db
}
models_test.go
package models
import (
"fmt"
"testing"
)
func TestGetGoods(t *testing.T) {
where := map[string]interface{}{
"goods_id": 1,
}
var goods Goods
DB().Debug().Where(where).Unscoped().Find(&goods)
fmt.Println("goods : ", goods)
}
测试运行(在当前代码目录下)
# go test -v
查询数据库返回结果。
gin框架封装(MVC结构)
框架基本结构
我们提供这个主要是后台服务或接口框架,根据前后端分离设计,前端可以使用vue/react封装组件来调用后台的接口或服务。该框架设计也结合了tp6的框架设计,目标是我们使用多语言开发微服务框架时,有一个更加统一的框架,减少不同语言隔阂带来理解和使用的开发成本。
app 应用基本根目录
admin,home,api 应用的接口模块层,主要封装控制层代码,admin是后台管理模块、home为pc端接口模块、api为移动端接口模块,该模块目录下(如user.go)为控制层逻辑代码。
model 应用的模型层,主要包含数据库对应ORM映射的模型代码
service 应用的服务层 ,主要提供封装比较常用的业务服务的逻辑代码
route 路由层 ,主要封装接口调用路由
config 配置,注意封装统一参数的配置,如setting.yml,我们通过yml格式配置。
util 工具包,包含各种工具类的封装。
go.mod golang框架依赖模块配置
main.go 框架入口
test.go 主要测试框架联通性等
首先,go.mod文件,我们看一下它的内容
我们看到“module ginframe2”,这是声明的应用的命名“ginframe2”,代码中可以作为引用的根名称,相当于命名空间。“go 1.15”是go的版本,require()其中时使用的第三方依赖包。
路由层示例代码
package route
import (
_ "ginframe2/app/api/controller" //一定要导入这个Controller包,用来注册需要访问的方法
ginAutoRouter "github.com/dengshulei/gin-auto-router"
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
//初始化路由
r := gin.Default()
//绑定基本路由,访问路径:/User/List
ginAutoRouter.Bind(r)
return r
}
其中,我们使用gin-auto-router组件包,能够实现自动路由添加,不必代码中维护修改路由,这样更方便代码开发。只需要将控制层包controller引入,自动匹配控制层的方法作为路由,类似于TP6的多模块自动扩展路由。
应用入口层main.go
package main
import (
"encoding/json"
"fmt"
"ginframe2/config"
"ginframe2/route"
)
func main() {
//加载配置
var config config.Config
conf := config.GetConf()
//将对象,转换成json格式
data_config, err := json.Marshal(conf)
if err != nil {
fmt.Println("err:\t", err.Error())
return
}
fmt.Println("data_config:\t", string(data_config))
//fmt.Println("config.Database.Driver=",config.Database.Driver);
//加载路由
r := route.InitRouter()
r.Run()
}
应用入口层统一定义了数据库连接、路由等资源的初始化,为MVC框架提供基础资源支持。其中统一定义配置文件(config/settings.yml)管理资源的常量,初始化后将配置文件解析为全局配置对象Config,便于代码中统一控制全局常量的引用。
配置文件(config/settings.yml)采用yaml格式,通过viper组件(github.com/spf13/viper)解析为Config结构。
Config.go示例代码
package config
import (
"fmt"
"github.com/spf13/viper"
"os"
)
type Config struct {
Database Database `yaml:"database"`
}
type Database struct {
Driver string `yaml:"driver"`
Source string `yaml:"source"`
}
//读取Yaml配置文件,并转换成Config对象 struct结构
func (config *Config) GetConf() *Config {
//获取项目的执行路径
path, err := os.Getwd()
if err != nil {
panic(err)
}
fmt.Println("path=",path)
vip := viper.New()
vip.AddConfigPath(path + "/config") //设置读取的文件路径
vip.SetConfigName("settings") //设置读取的文件名
vip.SetConfigType("yaml") //设置文件的类型
//尝试进行配置读取
if err := vip.ReadInConfig(); err != nil {
panic(err)
}
err = vip.Unmarshal(&config)
if err != nil {
panic(err)
}
return config
}
settings.yml示例
database:
# 数据库类型 mysql, sqlite3, postgres, sqlserver
driver: mysql
source: root:root@tcp(127.0.0.1:3306)/db_go_shop?charset=utf8&parseTime=True&loc=Local&timeout=1000ms
yaml文件格式,与解析结构Config对应。
工具包示例代码
package util
import (
"fmt"
"github.com/gin-gonic/gin"
"math"
"strconv"
)
//分页方法,根据传递过来的页数,每页数,总数,返回分页的内容 7个页数 前 1,2,3,4,5 后 的格式返回,小于5页返回具体页数
func Paginator(page, prepage int, nums int64) map[string]interface{} {
var firstpage int //前一页地址
var lastpage int //后一页地址
//根据nums总数,和prepage每页数量 生成分页总数
totalpages := int(math.Ceil(float64(nums) / float64(prepage))) //page总数
if page > totalpages {
page = totalpages
}
if page <= 0 {
page = 1
}
var pages []int
switch {
case page >= totalpages-5 && totalpages > 5: //最后5页
start := totalpages - 5 + 1
firstpage = page - 1
lastpage = int(math.Min(float64(totalpages), float64(page+1)))
pages = make([]int, 5)
for i, _ := range pages {
pages[i] = start + i
}
case page >= 3 && totalpages > 5:
start := page - 3 + 1
pages = make([]int, 5)
firstpage = page - 3
for i, _ := range pages {
pages[i] = start + i
}
firstpage = page - 1
lastpage = page + 1
default:
pages = make([]int, int(math.Min(5, float64(totalpages))))
for i, _ := range pages {
pages[i] = i + 1
}
firstpage = int(math.Max(float64(1), float64(page-1)))
lastpage = page + 1
//fmt.Println(pages)
}
paginatorMap := make(map[string]interface{})
paginatorMap["pages"] = pages
paginatorMap["totalpages"] = totalpages
paginatorMap["firstpage"] = firstpage
paginatorMap["lastpage"] = lastpage
paginatorMap["currpage"] = page
return paginatorMap
}
// Pagination is page util
func Pagination(ctx *gin.Context) (pageStr string, num int, err error) {
limit := ctx.DefaultQuery("page_size", "8")
pageNumber := ctx.DefaultQuery("page_number", "1")
limitInt, err := strconv.Atoi(limit)
if err != nil || limitInt < 0 {
return "", 0, err
}
pageNumberInt, err := strconv.Atoi(pageNumber)
if err != nil || pageNumberInt < 0 {
return "", 0, err
}
if pageNumberInt != 0 {
pageNumberInt--
}
offsetInt := limitInt * pageNumberInt
pageStr = fmt.Sprintf(" limit %d offset %d", limitInt+1, offsetInt)
return pageStr, limitInt, nil
}
提供工具类的封装,例如分页的方法。
控制层示例代码
package controller
import (
"encoding/json"
"fmt"
userService "ginframe2/app/service"
"ginframe2/util"
ginAutoRouter "github.com/dengshulei/gin-auto-router"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
func init() {
ginAutoRouter.Register(&User{})
}
type User struct {
}
//分页
func (api *User) Pages(c *gin.Context){
condMap := make(map[string]interface{})
//total :=userService.UserTotal(condMap)
page := c.DefaultQuery("page","1")
pageIndex ,err:=strconv.Atoi(page)
if(err!=nil){
panic(err)
}
pageSize :=2;
users,total:=userService.UserPage(condMap,pageIndex,pageSize)
pages:=util.Paginator(pageIndex,pageSize,total)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "ok",
"data": users,
"pages":pages,
})
}
//列表 带传参 可根据条件查询
func (api *User) List(c *gin.Context) {
userName := c.Query("name")
condMap := make(map[string]interface{})
if userName!="" {
condMap["name"]=userName
}
list := userService.UserList(condMap)
//结果序列化输出
listJsons,_:=json.Marshal(list)
fmt.Println("list==",list)
fmt.Println("listJsons==",listJsons)
c.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": "ok",
"data": "User:List="+string(listJsons),
})
}
//详细信息
func (api *User) UserInfo(c *gin.Context) {
}
其中,控制器调用service,执行相应逻辑返回结果。
服务层示例代码
package service
import (
"ginframe2/app/model"
)
//列表方法
// gorm的Unscoped方法设置tx.Statement.Unscoped为true;针对软删除会追加SoftDeleteDeleteClause,即设置deleted_at为指定的时间戳;而callbacks的Delete方法在db.Statement.Unscoped为false的时候才追加db.Statement.Schema.DeleteClauses,而Unscoped则执行的是物理删除。
func UserList(wheres map[string]interface{}) (list []model.User) {
var users []model.User
model.DB().Debug().Where(wheres).Unscoped().Order("id desc").Limit(5).Offset(8).Find(&users)
return users
}
//分页方法
func UserPage(wheres map[string]interface{},pageIndex int,pageSize int) (list []model.User,total int64){
var user []model.User
model.DB().Debug().Where(wheres).Find(&user).Count(&total)
offset := pageSize * (pageIndex - 1)
model.DB().Debug().Where(wheres).Unscoped().Order("id desc").Limit(pageSize).Offset(offset).Find(&list)
return list,total
}
//取得总行数
func UserTotal(wheres map[string]interface{}) int64{
var user []model.User
var total int64
model.DB().Debug().Where(wheres).Find(&user).Count(&total)
return total
}
服务层逻辑,使用比较多的比如涉及持久化CRUD的逻辑,其中按条件查询、分页功能使用的比较多,本示例列出这些比较常用的逻辑代码,代码比较通用、统一、简单,涵盖了golang的基本数据结构赋值、转换、数据库操作等,以此类推可以延伸出更多逻辑(不限于CRUD)。
模型层示例代码
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Id int `json:"userId" gorm:"primaryKey;autoIncrement;"` //对应表中的字段名 id
Name string `json:"name" gorm:"size:30;"` //name
Sex int `json:"sex" gorm:"size:1;"` //sex
}
package model
import (
"fmt"
"ginframe2/config"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
//取得数据库连接实例
func DB() *gorm.DB {
var config config.Config
conf := config.GetConf()
fmt.Println("model.DB().conf.Database.Source=",conf.Database.Source);
dsn :=conf.Database.Source //"root:root@tcp(localhost:3306)/db_go_shop?charset=utf8mb4&parseTime=true"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "tp_",
SingularTable: true,
},
})
fmt.Println("err : ", err)
return db
}
这里设计是model层独立抽象出来一个访问数据库的类model.go,每个model类如user.go只负责与db数据库的映射关系包装,而调用model进行访问出来,抽象到model.go的方法中,如获取db连接DB()等。
运行应用服务
Docker部署应用
docker安装参考:CentOS Docker 安装 | 菜鸟教程
服务器环境 Centos 7.9
$ curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
安装完后查看版本
$ docker -v
启动docker
$ sudo systemctl start docker
设置开机启动
$ sudo systemctl enable docker
编写Dockerfile
#源镜像
FROM golang:alpine
#将二进制文件拷贝进容器的GOPATH目录中
ADD ginfrm2 /go/src/ginframe2/ginfrm2
#为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
GOPROXY="https://goproxy.cn,direct"
#暴露端口
EXPOSE 3000
#工作目录
WORKDIR /go/src
#最终运行docker的命令 #此处运行容器内执行# CMD ["./ginfrm2"]
ENTRYPOINT ./ginfrm2
编译构建可执行文件
在源码目录下执行如下:
$ go build -o ginfrm2 main.go
注:-o参数后是生成的编译文件,windows下可以是.exe文件,后面是源码文件
执行后生成的可执行文件为ginfrm2
写到这里,生成的编译可执行文件为二进制的,其实可以直接使用可执行文件运行程序的,如下:
运行$ ./ginfrm2
默认启动服务器端口为8080,外部可以通过浏览器或postman直接访问相应的路由。
建立运行目录
$ mkdir /usr/go/goexe
将Dockerfile文件、可执行文件(ginfrm2)复制到运行目录goexe
$ cd goexe
$ cp /xxx/Dockerfile .
$ cp /xxx/ginfrm2 .
运行Dockerfile生产镜像
$ cd goexe
$ docker build -t ginframe2 -f Dockerfile .
查看镜像
$ docker images
运行镜像生成容器
docker run -p 3000:3000 --name ginFrm2 ginframe2
访问接口示例
通过postman测试调用接口,返回json格式响应数据。