引言

        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”,我们看看这个目录下内容

统一技术架构有什么优势 统一框架_github_02

主要为bin、pkg,pkg中就是安装的第三方包,再看截图

统一技术架构有什么优势 统一框架_统一技术架构有什么优势_03

pkg/mod下是安装的所有依赖包,这些包里面都是封装的go语言源码,我们在应用开发时,直接引用这些第三方依赖包(比如gin框架包) ,我们就可以不用自己写框架,框架的作用就是大牛们写好一些通用的源码,应用开发中会经常用到,不用重复发明同样的“柱子”,用这些大牛写的基础框架包或者工具、组件包,可以省去不少开发时间。

上述“go get”或“go install”是通过指令安装第三方包,实际应用开发中,可以把安装包组织到配置文件中通过“go mod tidy”指令统一安装,配置文件为go.mod,类似于php的composer打包配置工具(配置文件为composer.json),我们看看其中内容,如下:

统一技术架构有什么优势 统一框架_统一技术架构有什么优势_04

 我们执行“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

统一技术架构有什么优势 统一框架_gin_05

浏览器访问:http://127.0.0.1:8080/index

统一技术架构有什么优势 统一框架_github_06

示例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');

目录结构如下:

统一技术架构有什么优势 统一框架_golang_07

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

统一技术架构有什么优势 统一框架_封装_08

查询数据库返回结果。

gin框架封装(MVC结构)

框架基本结构

统一技术架构有什么优势 统一框架_golang_09

  

我们提供这个主要是后台服务或接口框架,根据前后端分离设计,前端可以使用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文件,我们看一下它的内容

统一技术架构有什么优势 统一框架_gin_10

 我们看到“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()等。

运行应用服务

统一技术架构有什么优势 统一框架_统一技术架构有什么优势_11

Docker部署应用

docker安装参考:CentOS Docker 安装 | 菜鸟教程

服务器环境 Centos 7.9

 $ curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

安装完后查看版本

$ docker -v

统一技术架构有什么优势 统一框架_gin_12

启动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

统一技术架构有什么优势 统一框架_封装_13

 默认启动服务器端口为8080,外部可以通过浏览器或postman直接访问相应的路由。

建立运行目录

$ mkdir /usr/go/goexe

将Dockerfile文件、可执行文件(ginfrm2)复制到运行目录goexe

$ cd goexe

$ cp /xxx/Dockerfile .

$ cp /xxx/ginfrm2 .

统一技术架构有什么优势 统一框架_github_14

运行Dockerfile生产镜像

$ cd goexe

$ docker build -t ginframe2 -f Dockerfile .

统一技术架构有什么优势 统一框架_golang_15

查看镜像

$ docker images

统一技术架构有什么优势 统一框架_github_16

 运行镜像生成容器

docker run -p 3000:3000 --name ginFrm2 ginframe2

访问接口示例

统一技术架构有什么优势 统一框架_统一技术架构有什么优势_17

 通过postman测试调用接口,返回json格式响应数据。