从0开始,用Go语言搭建一个简单的后端业务系统

Hello 小伙伴们,今日给我们带来了一份Go言语建立后端事务体系的教程,restful风格哦,既然是简略的事务体系,那么必要的功用就少不了增删改查,也就是传说中的CRUD,当然比较Spring Boot而言,Go言语写后端事务体系不是那么的流行,可是比照一下我们也很容易能发现,Go言语建立的Web后端体系的优势:

  • (1)内存占用更少
  • (2)发动速度更快
  • (3)代码愈加简练

OK,下面我们开端正文,首先看下完成后的成品吧:

从0开端,用Go言语建立一个简略的后端事务体系

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 后端代码

整体结构:

从0开端,用Go言语建立一个简略的后端事务体系

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="https://juejin.im/post/layui/css/layui.css">
</head>
<body>
<div>
    <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="https://juejin.im/post/layui/layui.js"></script>
<script src="https://juejin.im/post/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">' +
                        '  <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 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">' +
                '  <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

我正在参加技能社区创作者签约计划招募活动,点击链接报名投稿。

发表回复

提供最优质的资源集合

立即查看 了解详情