从 0 开始,用 Go 语言搭建一个简单的后端业务系统
- 2022 年 10 月 07 日 北京
本文字数:10345 字
阅读完需:约 34 分钟
Hello 小伙伴们,今天给大家带来了一份 Go 语言搭建后端业务系统的教程,restful 风格哦,既然是简单的业务系统,那么必要的功能就少不了增删改查,也就是传说中的 CRUD,当然相比 Spring Boot 而言,Go 语言写后端业务系统不是那么的流行,但是对比一下我们也很容易能发现,Go 语言搭建的 Web 后端系统的优势:
(1)内存占用更少
(2)启动速度更快
(3)代码更加简洁
OK,下面我们开始正文,首先看下完成后的成品吧:
1 业务 &技术概括
1.1 业务功能
实体(NumInfo):
Id:主键
Name :名称
InfoKey :Key
InfoNum :Num
业务功能:
根据 Key 将 Num 加 1
根据 Key 查找
根据 ID 查找
添加一个
根据 ID 删除一个
查看全部
根据 ID 修改一个
1.2 技术点
框架:
Gin
Viper
GORM
数据库:
MySQL
前端:
JQuery
layUI
2 接口规范
## 根据Key将Num加1
http://localhost:9888/add/{key}
### req:
http://localhost:9888/add/zs
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":"true"
}
```
## 根据Key查找
http://localhost:9888/findByKey/{key}
### req:
http://localhost:9888/findByKey/zs
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":{
"id":"1"
"name":"zs",
"info_key":"zs",
"info_num":"12"
}
}
```
## 根据ID查找
http://localhost:9888/findById/{id}
### req:
http://localhost:9888/findById/1
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":{
"id":"1"
"name":"zs",
"info_key":"zs",
"info_num":"12"
}
}
```
## 添加一个
http://localhost:9888/saveInfo
### req:
http://localhost:9888/saveInfo
```json
{
"name":"ww",
"info_key":"ww"
}
```
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":"true"
}
```
## 根据ID删除一个
http://localhost:9888/deleteInfo/{id}
### req:
http://localhost:9888/deleteInfo/1
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":"true"
}
```
## 查看全部
http://localhost:9888/getAll
### req:
http://localhost:9888/getAll
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":[{
"id":"1"
"name":"zs",
"info_key":"zs",
"info_num":"12"
},{
"id":"2"
"name":"ls",
"info_key":"ls",
"info_num":"12"
}]
}
```
## 根据ID修改一个
http://localhost:9888/update
### req:
http://localhost:9888/update
{
"id":"1"
"name":"zs",
"info_key":"zs",
"info_num":"12"
}
### resp:
```json
{
"code": "0",
"msg": "true",
"count": "0",
"data":"true"
}
```
3 后端代码
整体结构:
3.1 DAO 层
接口
package dao
import (
"context"
"count_num/pkg/entity"
)
type CountNumDAO interface {
//添加一个
AddNumInfo(ctx context.Context, info entity.NumInfo) bool
//根据Key获取一个
GetNumInfoByKey(ctx context.Context, url string) entity.NumInfo
//查看全部
FindAllNumInfo(ctx context.Context) []entity.NumInfo
//根据Key修改
UpdateNumInfoByKey(ctx context.Context, info entity.NumInfo) bool
//删除一个
DeleteNumInfoById(ctx context.Context, id int64) bool
//根据ID获取一个
GetNumInfoById(ctx context.Context, id int64) entity.NumInfo
//根据ID修改
UpdateNumInfoById(ctx context.Context, info entity.NumInfo) bool
}
实现
package impl
import (
"context"
"count_num/pkg/config"
"count_num/pkg/entity"
"gorm.io/gorm"
)
type CountNumDAOImpl struct {
db *gorm.DB
}
func NewCountNumDAOImpl() *CountNumDAOImpl {
return &CountNumDAOImpl{db: config.DB}
}
func (impl CountNumDAOImpl) AddNumInfo(ctx context.Context, info entity.NumInfo) bool {
var in entity.NumInfo
impl.db.First(&in, "info_key", info.InfoKey)
if in.InfoKey == info.InfoKey { //去重
return false
}
impl.db.Save(&info) //要使用指针
return true
}
func (impl CountNumDAOImpl) GetNumInfoByKey(ctx context.Context, key string) entity.NumInfo {
var info entity.NumInfo
impl.db.First(&info, "info_key", key)
return info
}
func (impl CountNumDAOImpl) FindAllNumInfo(ctx context.Context) []entity.NumInfo {
var infos []entity.NumInfo
impl.db.Find(&infos)
return infos
}
func (impl CountNumDAOImpl) UpdateNumInfoByKey(ctx context.Context, info entity.NumInfo) bool {
impl.db.Model(&entity.NumInfo{}).Where("info_key = ?", info.InfoKey).Update("info_num", info.InfoNum)
return true
}
func (impl CountNumDAOImpl) DeleteNumInfoById(ctx context.Context, id int64) bool {
impl.db.Delete(&entity.NumInfo{}, id)
return true
}
func (impl CountNumDAOImpl) GetNumInfoById(ctx context.Context, id int64) entity.NumInfo {
var info entity.NumInfo
impl.db.First(&info, "id", id)
return info
}
func (impl CountNumDAOImpl) UpdateNumInfoById(ctx context.Context, info entity.NumInfo) bool {
impl.db.Model(&entity.NumInfo{}).Where("id", info.Id).Updates(entity.NumInfo{Name: info.Name, InfoKey: info.InfoKey, InfoNum: info.InfoNum})
return true
}
3.2 Web 层
package controller
import (
"bytes"
"count_num/pkg/dao/impl"
"count_num/pkg/entity"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"io/ioutil"
"strconv"
)
type NumInfoControllerImpl struct {
dao *impl.CountNumDAOImpl
}
type NumInfoController interface {
AddNumByKey(c *gin.Context)
FindNumByKey(c *gin.Context)
SaveNumInfo(c *gin.Context)
DeleteById(c *gin.Context)
FindAll(c *gin.Context)
FindNumById(c *gin.Context)
Update(context *gin.Context)
}
func NewNumInfoControllerImpl() *NumInfoControllerImpl {
return &NumInfoControllerImpl{dao: impl.NewCountNumDAOImpl()}
}
func (impl NumInfoControllerImpl) AddNumByKey(c *gin.Context) {
key := c.Param("key")
numInfo := impl.dao.GetNumInfoByKey(c, key)
numInfo.InfoNum = numInfo.InfoNum + 1
isOk := impl.dao.UpdateNumInfoByKey(c, numInfo)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": isOk})
}
func (impl NumInfoControllerImpl) FindNumByKey(c *gin.Context) {
key := c.Param("key")
numInfo := impl.dao.GetNumInfoByKey(c, key)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": numInfo})
}
func (impl NumInfoControllerImpl) SaveNumInfo(c *gin.Context) {
body := c.Request.Body
bytes, err := ioutil.ReadAll(body)
info := entity.NumInfo{}
json.Unmarshal(bytes, &info)
if err != nil {
panic(err)
}
isOk := impl.dao.AddNumInfo(c, info)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": isOk})
}
func (impl NumInfoControllerImpl) DeleteById(c *gin.Context) {
id := c.Param("id")
i, _ := strconv.Atoi(id)
isOk := impl.dao.DeleteNumInfoById(c, int64(i))
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": isOk})
}
func (impl NumInfoControllerImpl) FindAll(c *gin.Context) {
numInfos := impl.dao.FindAllNumInfo(c)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": len(numInfos), "data": numInfos})
}
func (impl NumInfoControllerImpl) FindNumById(c *gin.Context) {
id := c.Param("id")
i, _ := strconv.Atoi(id)
numInfo := impl.dao.GetNumInfoById(c, int64(i))
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": numInfo})
}
func (impl NumInfoControllerImpl) Update(c *gin.Context) {
body := c.Request.Body
jsonBytes, err := ioutil.ReadAll(body)
d := json.NewDecoder(bytes.NewReader(jsonBytes))
d.UseNumber()
m := make(map[string]string)
d.Decode(&m)
if err != nil {
panic(err)
}
info := entity.NumInfo{
Id: cast.ToInt64(m["id"]),
Name: m["name"],
InfoKey: m["info_key"],
InfoNum: cast.ToInt64(m["info_num"]),
}
isOk := impl.dao.UpdateNumInfoById(c, info)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": isOk})
}
拦截器配置
package interceptor
import (
"github.com/gin-gonic/gin"
"log"
)
// HttpInterceptor 自定义拦截器
func HttpInterceptor() gin.HandlerFunc {
return func(c *gin.Context) {
// 设置 example 变量
c.Set("example", "12345")
// 请求前
log.Print("--------------拦截器-------------")
//定义错误,终止并返回该JSON
//c.AbortWithStatusJSON(500, "error")
//requestURI := c.Request.RequestURI
//fmt.Println(requestURI)
//通过请求
c.Next()
}
}
router
package web
import (
"count_num/pkg/config"
"count_num/pkg/web/controller"
"count_num/pkg/web/interceptor"
"github.com/gin-gonic/gin"
)
func RunHttp() {
r := gin.Default()
//增加拦截器
r.Use(interceptor.HttpInterceptor())
//解决跨域
r.Use(config.CorsConfig())
//路由组
appInfoGroup := r.Group("/")
{
appInfoGroup.POST("/add/:key", controller.NewNumInfoControllerImpl().AddNumByKey)
appInfoGroup.GET("/findByKey/:key", controller.NewNumInfoControllerImpl().FindNumByKey)
appInfoGroup.GET("/findById/:id", controller.NewNumInfoControllerImpl().FindNumById)
appInfoGroup.POST("/saveInfo", controller.NewNumInfoControllerImpl().SaveNumInfo)
appInfoGroup.POST("/deleteInfo/:id", controller.NewNumInfoControllerImpl().DeleteById)
appInfoGroup.GET("/getAll", controller.NewNumInfoControllerImpl().FindAll)
appInfoGroup.POST("/update", controller.NewNumInfoControllerImpl().Update)
}
r.Run("127.0.0.1:" + config.PORT)
}
3.3 配置和实体结构体
package config
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func init() {
var err error
viper.SetConfigName("app")
viper.SetConfigType("properties")
viper.AddConfigPath("./")
err = viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %w \n", err))
}
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("No file ...")
} else {
fmt.Println("Find file but have err ...")
}
}
PORT = viper.GetString("server.port")
url := viper.GetString("db.url")
db := viper.GetString("db.databases")
username := viper.GetString("db.username")
password := viper.GetString("db.password")
dsn := username + ":" + password + "@tcp(" + url + ")/" + db + "?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
}
跨域问题
package config
import (
"github.com/gin-gonic/gin"
"net/http"
)
var PORT string
func CorsConfig() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(200)
} else {
c.Next()
}
}
}
实体
package entity
type NumInfo struct {
Id int64 `json:"id"`
Name string `json:"name"`
InfoKey string `json:"info_key"`
InfoNum int64 `json:"info_num"`
}
func (stu NumInfo) TableName() string {
return "num_info"
}
3.4 启动主函数
package main
import "count_num/pkg/web"
func main() {
web.RunHttp()
}
3.5 配置文件和 SQL
server.port=9888
db.driver=mysql
db.url=127.0.0.1:3306
db.databases=test
db.username=root
db.password=12345
SQL 文件
CREATE TABLE `num_info`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`info_key` varchar(255) NOT NULL,
`info_num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`, `info_key`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;
4 前端代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>后台管理</title>
<link rel="stylesheet" href="./layui/css/layui.css">
</head>
<body>
<div style="margin-left: 220px;position: absolute;top: 70px;width: 80%;">
<button class="layui-btn layui-btn-normal" id="add_btn" lay-filter="add_btn">添加一个</button>
<table id="demo" lay-filter="test"></table>
</div>
<script src="./layui/layui.js"></script>
<script src="./layui/jquery.min.js"></script>
<script>
var base_url = 'http://localhost:9888'
layui.use('element', function () {
var element = layui.element;
});
layui.use('table', function () {
var table = layui.table;
//第一个实例
table.render({
elem: '#demo'
, height: 500
, url: base_url + '/getAll' //数据接口
, page: false //开启分页
, cols: [[ //表头
{field: 'id', title: 'ID', width: '10%'}
, {field: 'name', title: '名称', width: '30%'}
, {field: 'info_key', title: '标记', sort: true, width: '20%'}
, {field: 'info_num', title: '次数', width: '20%', sort: true}
, {title: '操作', width: '20%', toolbar: '#barDemo'}
]]
});
//工具条事件
table.on('tool(test)', function (obj) {
var data = obj.data;
var layEvent = obj.event;
var tr = obj.tr;
if ('edit' == layEvent) {
layer.open({
title: '编辑',
type: 1,
content: '<form class="layui-form" style="margin-top: 20px;margin-right: 60px;">' +
' <div class="layui-form-item">\n' +
' <label class="layui-form-label">名称</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input" id="u_name" value="' + data.name + '">\n' +
' </div>\n' +
' </div>' +
' <div class="layui-form-item">\n' +
' <label class="layui-form-label">标记</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input" id="u_key" value="' + data.info_key + '">\n' +
' </div>\n' +
' </div>' +
' <div class="layui-form-item">\n' +
' <label class="layui-form-label">次数</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input" id="u_num" value="' + data.info_num + '">\n' +
' </div>\n' +
' </div>' +
'<div class="layui-form-item">\n' +
' <div class="layui-input-block">\n' +
' <button class="layui-btn" type="button" onclick="update()">确定修改</button>\n' +
' <button type="reset" class="layui-btn layui-btn-primary">重置</button>\n' +
' </div>\n' +
' </div>' +
' <input style="display: none;" id="u_id" value="' + data.id + '">' +
'</form>'
});
} else if ('del' == layEvent) {
layer.confirm('确定删除吗?', function (index) {
//点击确认时执行
$.ajax({
url: base_url + '/deleteInfo/'+data.id,
type: 'POST',
success: function (r) {
if (r.data) {
location.reload();
}
}
})
layer.close(index);
});
}
});
});
$('#add_btn').on('click', function () {
layer.open({
title: '添加',
type: 1,
content: '<form class="layui-form" style="margin-top: 20px;margin-right: 60px;">' +
' <div class="layui-form-item">\n' +
' <label class="layui-form-label">名称</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input" id="s_name">\n' +
' </div>\n' +
' </div>' +
' <div class="layui-form-item">\n' +
' <label class="layui-form-label">标记</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input" id="s_key">\n' +
' </div>\n' +
' </div>' +
'<div class="layui-form-item">\n' +
' <div class="layui-input-block">\n' +
' <button class="layui-btn" onclick="save()" type="button">立即提交</button>\n' +
' <button type="reset" class="layui-btn layui-btn-primary">重置</button>\n' +
' </div>\n' +
' </div>' +
'</form>'
});
});
</script>
<script>
function save() {
var name = $("#s_name").val();
var key = $("#s_key").val();
$.ajax({
url: base_url + '/saveInfo',
type: 'POST',
data: JSON.stringify({"name": name, "info_key": key}),
contentType: 'application/json',
success: function (r) {
if (r.data) {
location.reload();
}
}
})
}
function update() {
var id = $("#u_id").val();
var name = $("#u_name").val();
var key = $("#u_key").val();
var num = $("#u_num").val();
$.ajax({
url: base_url + '/update',
type: 'POST',
data: JSON.stringify({"id": id, "name": name, "info_key": key, "info_num": num}),
contentType: 'application/json',
success: function (r) {
if (r.data) {
location.reload();
}
}
})
}
</script>
<script type="text/html" id="barDemo">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</body>
</html>
5 遇见的问题及排查方式
5.1 GORM 的使用问题
5.1.1 自定义表名
func (stu NumInfo) TableName() string {
return "num_info"
}
5.1.2 主键自增
impl.db.Save(&info) //要使用指针
5.2 跨域问题
func CorsConfig() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(200)
} else {
c.Next()
}
}
}
在 Gin 中解决跨域问题
func RunHttp() {
r := gin.Default()
......
//解决跨域
r.Use(config.CorsConfig())
......
r.Run("127.0.0.1:" + config.PORT)
}
5.3 JSON 字段转 int64 失效问题
func (impl NumInfoControllerImpl) Update(c *gin.Context) {
body := c.Request.Body
jsonBytes, err := ioutil.ReadAll(body)
//先将JSON转为Map
d := json.NewDecoder(bytes.NewReader(jsonBytes))
d.UseNumber()
m := make(map[string]string)
d.Decode(&m)
if err != nil {
panic(err)
}
//再将Map转为实体
info := entity.NumInfo{
Id: cast.ToInt64(m["id"]),
Name: m["name"],
InfoKey: m["info_key"],
InfoNum: cast.ToInt64(m["info_num"]),
}
isOk := impl.dao.UpdateNumInfoById(c, info)
c.JSON(200, map[string]interface{}{"code": 0, "msg": "", "count": 0, "data": isOk})
}
6 总结
好了,今天的分享就到这里,虽然学习了很长时间的 Go 语言,但是搭建这样较为完整的业务系统的机会不是很多,过程中也遇到了几个问题,但是都利用官方文档或搜索引擎独立的解决了。
当然目前的后端业务系统只支持 restful 风格的 Http 请求,如果后续有时间的话还会增加相同功能的 rpc 接口来做扩展,相关的 GitHub 地址分享给大家,如果有哪些地方需要改良和优化,还大家请多多指教!
源码获取方式:关注公众号[ 扯编程的淡 ],回复:0615
版权声明: 本文为 InfoQ 作者【Barry Yan】的原创文章。
原文链接:【http://xie.infoq.cn/article/04d9b70f314712330940625a2】。文章转载请联系作者。
Barry Yan
做兴趣使然的Hero 2021.01.14 加入
Just do it.
评论