写点什么

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

作者:Barry Yan
  • 2022 年 10 月 07 日
    北京
  • 本文字数:10345 字

    阅读完需:约 34 分钟

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

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加1http://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=mysqldb.url=127.0.0.1:3306db.databases=testdb.username=rootdb.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

发布于: 刚刚阅读数: 4
用户头像

Barry Yan

关注

做兴趣使然的Hero 2021.01.14 加入

Just do it.

评论

发布
暂无评论
从0开始,用Go语言搭建一个简单的后端业务系统_10月月更_Barry Yan_InfoQ写作社区