Go-gin-example 添加注释 第一部分 新建项目及api编写

news2024/11/15 18:01:22

文章目录

  • go-gin-example
  • 环境准备
    • 初始化 Go Modules
      • 基础使用
    • gin 安装
      • 测试gin是否引入
  • gin搭建Blog API's
    • go-ini
      • 简述配置文件
    • 阶段目标 编写简单API错误码包 完成一个demo
      • 初始化项目
      • 初始化项目数据库
      • 编写项目配置包
        • 拉取go-ini配置包
        • 在conf目录下新建app.ini文件,写入内容:
        • 建立调用配置的`setting` 模块
        • 编写 API 错误码包
        • 编写工具包
          • 编写分页页码的获取方法
        • 编写 models init
      • 编写项目启动、路由文件 Demo
      • 知识点部分
      • 最后的小改进部分
    • 编写tag的api
      • 定义接口
      • 编写路由空壳
      • 注册路由
      • 检查路由是否注册成功
      • 下载依赖包
      • 编写标签列表的 models 逻辑
      • 编写标签列表的路由逻辑
      • 编写新增标签的 models 逻辑
      • 编写新增标签的路由逻辑
      • 编写 models callbacks
      • 编写其余接口的路由逻辑
      • 编写其余接口的 models 逻辑
      • 验证功能
    • 完成博客的文章类接口定义和编写
      • 定义接口
      • 编写路由空壳
      • 编写models逻辑
        • 逻辑接口
      • 编写路由逻辑
    • 验证功能

go-gin-example

=_= 学习煎鱼大佬的该项目
煎鱼大佬网站
原github地址

个人github项目地址,希望得到一点star

环境准备

初始化 Go Modules

Go Modules 是go的依赖包管理工具,现在的go版本自动打开modules,目前的go get命令也是需要进行初始化才能进行拉取

在准备的文件夹的终端中执行

$ go env -w GOPROXY=https://goproxy.cn,direct

$ go mod init [github.com/kingsill/gin-example]

go env -w GOPROXY=...:设置 GOPROXY 代理,这里主要涉及到两个值,第一个是 https://goproxy.cn,它是由七牛云背书的一个强大稳定的 Go 模块代理,可以有效地解决你的外网问题;第二个是 direct,它是一个特殊的 fallback 选项,它的作用是用于指示 Go 在拉取模块时遇到错误会回源到模块版本的源地址去抓取(比如 GitHub 等)。

go mod init [MODULE_PATH]:初始化 Go modules,它将会生成 go.mod 文件,需要注意的是 MODULE_PATH 填写的是模块引入路径,你可以根据自己的情况修改路径。
这里我们使用github域名作为项目名是证明这个包使用github进行存储。

此时 .mod文件内容。是当前的 模块路径 和 预期的 Go 语言版本

module github.com/kingsill/gin-example

go 1.21.4

基础使用

  • 用 go get 拉取新的依赖
    • 拉取最新的版本(优先择取 tag):go get golang.org/x/text@latest
    • 拉取 master 分支的最新 commit:go get golang.org/x/text@master
    • 拉取 tag 为 v0.3.2 的 commit:go get golang.org/x/text@v0.3.2
    • 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:go get golang.org/x/text@342b2e
    • 用 go get -u 更新现有的依赖
    • 用 go mod download 下载 go.mod 文件中指明的所有依赖
    • 用 go mod tidy 整理现有的依赖
    • 用 go mod graph 查看现有的依赖结构
    • 用 go mod init 生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令)
  • 用 go mod edit 编辑 go.mod 文件
  • 用 go mod vendor 导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念)
  • 用 go mod verify 校验一个模块是否被篡改过

gin 安装

在项目的根目录的命令行执行

$ go get -u github.com/gin-gonic/gin

此时我们的目录如下所示,多出.sum文件
go.sum 文件详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。

gin-example learn
├─ .idea
│  └─ workspace.xml
├─ go.mod
├─ go.sum
└─ README.md

此时go.mod文件也多了一些内容。

module github.com/kingsill/gin-example

go 1.21.4

require (
	github.com/bytedance/sonic v1.10.2 // indirect
	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
	github.com/chenzhuoyu/iasm v0.9.1 // indirect
	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/gin-gonic/gin v1.9.1 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.16.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
	github.com/leodido/go-urn v1.2.4 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.12 // indirect
	golang.org/x/arch v0.6.0 // indirect
	golang.org/x/crypto v0.17.0 // indirect
	golang.org/x/net v0.19.0 // indirect
	golang.org/x/sys v0.15.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	google.golang.org/protobuf v1.32.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

go.mod 文件是启用了 Go modules 的项目所必须的最重要的文件,因为它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头,目前有以下 5 个动词:

  • module:用于定义当前项目的模块路径。
  • go:用于设置预期的 Go 版本。
  • require:用于设置一个特定的模块版本。
  • exclude:用于从使用中排除一个特定的模块版本。
  • replace:用于将一个模块版本替换为另外一个模块版本。

你可能还会疑惑 indirect 是什么东西,indirect 的意思是传递依赖,也就是非直接依赖。

测试gin是否引入

gin部分学习博客

编写test.go文件并执行

package main

import "github.com/gin-gonic/gin"

func main() {
	e := gin.Default()
	e.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "pong"})
	})
	e.Run()
}

访问 127.0.0.1:8080,如下则正确安装
在这里插入图片描述

此时我们go.mod文件中使用的依赖 后缀也 都是indirect,这时候我们需要使用go mod tidy进行依赖整理,这个命令非常常用

整理完之后go.mod文件如下所示:gin已经是直接依赖

module go-gin-example

go 1.21.4

require github.com/gin-gonic/gin v1.9.1

require (
	github.com/bytedance/sonic v1.10.2 // indirect
	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
	github.com/chenzhuoyu/iasm v0.9.1 // indirect
	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.16.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
	github.com/leodido/go-urn v1.2.4 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.12 // indirect
	golang.org/x/arch v0.6.0 // indirect
	golang.org/x/crypto v0.17.0 // indirect
	golang.org/x/net v0.19.0 // indirect
	golang.org/x/sys v0.15.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	google.golang.org/protobuf v1.32.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

gin搭建Blog API’s

由于要避免以下问题:

  • 程序的文本配置写在代码中
  • API的错误码硬编码在程序中
  • DB句柄谁都open,没有通一管理
  • 获取分页等公共参数 一人一套逻辑
    我们选择进行配置文件,这里我们选择go-ini/ini,要先简单阅读其中文文档

go-ini

简述配置文件

配置文件本质上是包含成功操作程序所需信息的文件,这些信息以特定方式构成。是用户可配置的,通常存储在纯文本文件中

配置文件可以是各种格式,完全凭借程序员的发挥,不过出于方便,大部分会选择的配置文件格式集中在那几种.一般而言程序启动时,会加载该程序对应的配置文件内的信息

有这么几点作用
1.数据库的连接工作
2.端口号的配置
3.打印日志等等

阶段目标 编写简单API错误码包 完成一个demo

初始化项目

将目录结构更新到如下所示:

go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
  • conf:用于存储配置文件
  • middleware:应用中间件
  • models:应用数据库模型
  • pkg:第三方包
  • routers 路由逻辑处理
  • runtime:应用运行时数据

初始化项目数据库

新建 blog 数据库,编码为utf8_general_ci,在 blog 数据库下,新建以下表

  1. 标签表
CREATE TABLE `blog_tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT '' COMMENT '标签名称',
  `created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
  `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';
  1. 文章表
CREATE TABLE `blog_article` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',
  `title` varchar(100) DEFAULT '' COMMENT '文章标题',
  `desc` varchar(255) DEFAULT '' COMMENT '简述',
  `content` text,
  `created_on` int(11) DEFAULT NULL,
  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
  `modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
  1. 认证表
CREATE TABLE `blog_auth` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT '' COMMENT '账号',
  `password` varchar(50) DEFAULT '' COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');

编写项目配置包

拉取go-ini配置包
```
go get -u github.com/go-ini/ini
```
在conf目录下新建app.ini文件,写入内容:
[ini中文文档](https://ini.unknwon.io/docs/intro/getting_started)

```ini
#debug or release
RUN_MODE = debug

[app]
PAGE_SIZE = 10
JWT_SECRET = 23347$040412

[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60

[database]
TYPE = mysql
USER = 数据库账号
PASSWORD = 数据库密码
#127.0.0.1:3306
HOST = 数据库IP:数据库端口号
NAME = blog
TABLE_PREFIX = blog_
```
**相关内容记得填写自己的配置进去**
可以看到`ini配置`文件中,`默认分区`中定义RUN_MODE为debug,`app分区`中定义两项,`server分区`定义HTTP_PORT等,`database分区`定义了数据库相关信息
>个人理解:ini配置文件将我们可能用到的配置信息同意展示在配置文件里,有助于后续管理及更新
建立调用配置的setting 模块
go-gin-example的`pkg`目录下新建`setting目录`,新建 `setting.go` 文件,写入内容:

```go
package setting

import (
"log"
"time"

"github.com/go-ini/ini"
)

var (
Cfg *ini.File //加载配置文件读取

RunMode string

HTTPPort     int
ReadTimeout  time.Duration
WriteTimeout time.Duration

PageSize  int
JwtSecret string
)

func init() {
var err error
Cfg, err = ini.Load("conf/app.ini") //加载cong/app.ini文件,即我们自己创建的配置文件
if err != nil {                     //错误提示
	log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
}

LoadBase()
LoadServer()
LoadApp()
}

// LoadBase 加载运行模式
func LoadBase() {
//默认分区,使用"",key值为RUN_MODE,若配置中未指定,则默认为debug
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
}

// LoadServer 加载服务器相关配置
func LoadServer() {
	sec, err := Cfg.GetSection("server") //从配置文件检索server分区的内容,sec为获取的指定分区
	if err != nil {                      //错误提示
		log.Fatalf("Fail to get section 'server': %v", err)
	}

	//设置服务器配置,如果配置中未指定则使用默认值。
	HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
	ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
	WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
}

// LoadApp 从配置文件中加载应用程序特定的设置
func LoadApp() {
	sec, err := Cfg.GetSection("app") //从配置文件检索app分区的内容,sec为获取的指定分区
	if err != nil {                   //错误提示
		log.Fatalf("Fail to get section 'app': %v", err)
	}
	
	// 设置应用程序配置,如果配置中未指定则使用默认值。
	JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
	PageSize = sec.Key("PAGE_SIZE").MustInt(10)
}

```

当前目录结构
```
go-gin-example
├── conf
│   └── app.ini
├─middleware
├─models
├─pkg
│  └─setting
|       └──setting.go
├─routers
└─runtime
```
编写 API 错误码包

建立错误码的e模块,在go-gin-examplepkg目录下新建e目录,新建code.gomsg.go文件,写入内容:

这里即创建golang中的枚举方法,便于后期处理和维护,GO GORM 自定义数据类型-枚举这边文章具体提及其方法,可以参考

  1. code.go
    在code.go中 定义 具有意义的字符 为 对应的错误码,具有编码作用

    package e
    
    const (
    	SUCCESS = 200
    	ERROR = 500
    	INVALID_PARAMS = 400
    
    	ERROR_EXIST_TAG = 10001
    	ERROR_NOT_EXIST_TAG = 10002
    	ERROR_NOT_EXIST_ARTICLE = 10003
    
    	ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
    	ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
    	ERROR_AUTH_TOKEN = 20003
    	ERROR_AUTH = 20004
    )
    
  2. msg.go
    msg.go中 通过建立 int对应string的map表 将 code.go 中的字符常量 对应为 字符串,之后 通过GetMsg函数 可以直接将 错误码 对应到 想要传递的错误信息

    package e
    
    var MsgFlags = map[int]string {
    	SUCCESS : "ok",
    	ERROR : "fail",
    	INVALID_PARAMS : "请求参数错误",
    	ERROR_EXIST_TAG : "已存在该标签名称",
    	ERROR_NOT_EXIST_TAG : "该标签不存在",
    	ERROR_NOT_EXIST_ARTICLE : "该文章不存在",
    	ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鉴权失败",
    	ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超时",
    	ERROR_AUTH_TOKEN : "Token生成失败",
    	ERROR_AUTH : "Token错误",
    }
    
    func GetMsg(code int) string {
    	msg, ok := MsgFlags[code]
    	if ok {
    		return msg
    	}
    
    	return MsgFlags[ERROR]
    }
    
编写工具包

go-gin-examplepkg目录下新建util目录,并拉取com的依赖包,如下:

$ go get -u github.com/unknwon/com
编写分页页码的获取方法

util目录下新建pagination.go,写入内容:

package util

import (
	"github.com/gin-gonic/gin"
	"github.com/unknwon/com"

	"github.com/kingsill/gin-example/pkg/setting"
)

// GetPage page 1  0;page 2  10;page 3  20
func GetPage(c *gin.Context) int {
	result := 0                                 //默认查询结果为0,及第1页
	page, _ := com.StrTo(c.Query("page")).Int() //查询url中包含的page信息并将其转化为int类型
	if page > 0 {                               //如果获取的页数大于0
		result = (page - 1) * setting.PageSize //返回(页数-1)×每页大小(在setting.go中已经配置过)
	}

	return result
}

关于其中gin的查询参数部分可以参考这部分gin 查询参数

编写 models init

//配置部分数据库连接相关内容
拉取gorm的依赖包,拉取mysql驱动的依赖包,如下:

$ go get -u github.com/jinzhu/gorm
$ go get -u github.com/go-sql-driver/mysql

有关gorm和mysql可以参看这两部分文章:
gorm mysql

完成后,在go-gin-examplemodels目录下新建models.go,用于models的初始化使用

package models

import (
	"fmt"
	"log"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/kingsill/gin-example/pkg/setting"
)

// 定义一个全局的数据库连接变量
var db *gorm.DB

type Model struct {
	ID         int `gorm:"primary_key" json:"id"`
	CreatedOn  int `json:"created_on"`
	ModifiedOn int `json:"modified_on"`
}

// 将一下定义为init函数
func init() {
	var (
		err                                               error
		dbType, dbName, user, password, host, tablePrefix string
	)

	//加载配置文件中database分区的数据
	sec, err := setting.Cfg.GetSection("database") //cfj在setting模块中已经通过init函数进行初始化
	if err != nil {
		log.Fatal(2, "Fail to get section 'database': %v", err)
	}

	//配置导入
	dbType = sec.Key("TYPE").String()
	dbName = sec.Key("NAME").String()
	user = sec.Key("USER").String()
	password = sec.Key("PASSWORD").String()
	host = sec.Key("HOST").String()
	tablePrefix = sec.Key("TABLE_PREFIX").String()

	//使用gorm框架初始化数据库连接
	db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
		user,
		password,
		host,
		dbName))
	if err != nil {
		log.Println(err)
	}

	//自定义默认表的表名,使用匿名函数,在原默认表名的前面加上配置文件中定义的前缀
	gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
		return tablePrefix + defaultTableName
	}

	//gorm默认使用复数映射,当前设置后即进行严格匹配
	db.SingularTable(true)
	//log记录打开
	db.LogMode(true)

	//进行连接池设置
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(100)
}

// CloseDB 与数据库断开连接函数
func CloseDB() {
	defer db.Close()
}

此时我依旧不太明白中间闭包的作用,还希望路过大神能够解惑

编写项目启动、路由文件 Demo

在go-gin-example下建立main.go作为启动文件(也就是main包),我们先写个Demo,帮助大家理解,写入文件内容:

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"

	"github.com/kingsill/gin-example/pkg/setting"
)

// 这里的测试demo只使用到了pkg/setting包,即读取配置文件app. ini文件部分
func main() {

	//创建一个默认路由器router
	router := gin.Default()

	//创建一个对应的路由handler处理get请求,这里定义了测试的ip即x.x.x/test
	router.GET("/test", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "test",
		})
	})

	//创建一个http服务器,将前面的router绑定为这里的处理器
	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	//启动服务器
	s.ListenAndServe()
}

执行go run main.go,查看命令行是否显示:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /test                     --> main.main.func1 (3 handlers)

在本机执行curl 127.0.0.1:8000/test,或者在浏览器中输入ip地址,检查是否返回{“message”:“test”}。

笔者的路由、handler等定义可能比较混乱,也希望大家指出错误

知识点部分

那么,我们来延伸一下 Demo 所涉及的知识点!

标准库

  • fmt:实现了类似 C 语言 printf 和 scanf 的格式化 I/O。格式化动作(‘verb’)源自 C 语言但更简单
    net/http:提供了 HTTP 客户端和服务端的实现

Gin

  • gin.Default():返回 Gin 的type Engine struct{…},里面包含RouterGroup,相当于创建一个路由Handlers,可以后期绑定各类的路由规则和函数、中间件等
  • router.GET(…){…}:创建不同的 HTTP 方法绑定到Handlers中,也支持 POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等常用的 Restful 方法
  • gin.H{…}:就是一个map[string]interface{}
  • gin.Context:Context是gin中的上下文,它允许我们在中间件之间传递变量、管理流、验证 JSON 请求、响应 JSON 请求等,在gin中包含大量Context的方法,例如我们常用的DefaultQuery、Query、DefaultPostForm、PostForm等等

&http.Server 和 ListenAndServe?

  1. http.Server:
type Server struct {
    Addr    string
    Handler Handler
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
}
  • Addr:监听的 TCP 地址,格式为:8000
  • Handler:http 句柄,实质为ServeHTTP,用于处理程序响应 HTTP 请求
  • TLSConfig:安全传输层协议(TLS)的配置
  • ReadTimeout:允许读取的最大时间
  • ReadHeaderTimeout:允许读取请求头的最大时间
  • WriteTimeout:允许写入的最大时间
  • IdleTimeout:等待的最大时间
  • MaxHeaderBytes:请求头的最大字节数
  • ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
  • ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为nil则默认以日志包的标准日志记录器完成(也就是在控制台输出)
  1. ListenAndServe:
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

开始监听服务,监听 TCP 网络地址,Addr 和调用应用程序处理连接上的请求。

我们在源码中看到Addr是调用我们在&http.Server中设置的参数,因此我们在设置时要用&,我们要改变参数的值,因为我们ListenAndServe和其他一些方法需要用到&http.Server中的参数,他们是相互影响的。

  1. http.ListenAndServe和 连载一 的r.Run()有区别吗?
    我们看看r.Run的实现:
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

通过分析源码,得知本质上没有区别,同时也得知了启动gin时的监听 debug 信息在这里输出。

最后的小改进部分

Demo 的router.GET等路由规则可以不写在main包中吗?
我们发现router.GET等路由规则,在 Demo 中被编写在了main包中,感觉很奇怪,我们去抽离这部分逻辑!

在go-gin-example下routers目录新建router.go文件,写入内容:

package routers

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {

	//创建新的router而不是默认。
	r := gin.New()
	
	//定义router的配置
    r.Use(gin.Logger())
    r.Use(gin.Recovery())
    gin.SetMode(setting.RunMode)

	//将处理函数搬到这里
    r.GET("/test", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "test",
        })
    })

    return r
}

修改main.go文件内容

package main

import (
	"fmt"
	"net/http"

	"github.com/EDDYCJY/go-gin-example/routers"
	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func main() {
	router := routers.InitRouter()

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	s.ListenAndServe()
}

当前目录结构:

go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   └── models.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   └── router.go
├── runtime

编写tag的api

目标:完成博客的标签类接口定义和编写

定义接口

本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!

  • 获取标签列表:GET("/tags”)
  • 新建标签:POST("/tags”)
  • 更新指定标签:PUT("/tags/:id”)
  • 删除指定标签:DELETE("/tags/:id”)

编写路由空壳

开始编写路由文件逻辑,在routers下新建api目录,我们当前是第一个 API 大版本,因此在api下新建v1目录,再新建tag.go文件,写入内容:

package v1

import (
    "github.com/gin-gonic/gin"
)

//获取多个文章标签
func GetTags(c *gin.Context) {
}

//新增文章标签
func AddTag(c *gin.Context) {
}

//修改文章标签
func EditTag(c *gin.Context) {
}

//删除文章标签
func DeleteTag(c *gin.Context) {
}

注册路由

我们打开routers下的router.go文件,修改文件内容为:

package routers

import (
	"github.com/gin-gonic/gin"

	"github.com/kingsill/gin-example/pkg/setting"
	"github.com/kingsill/gin-example/routers/api/v1"
)

func InitRouter() *gin.Engine {
	//注册一个新的router
	r := gin.New()

	//使用logger
	r.Use(gin.Logger())

	r.Use(gin.Recovery())

	//将运行模式放到setting中设置的模式上
	gin.SetMode(setting.RunMode)

	//路由分组,统一管理,统一增加 前缀
	apiv1 := r.Group("/api/v1")
	{
		//获取标签列表
		apiv1.GET("/tags", v1.GetTags)
		//新建标签
		apiv1.POST("/tags", v1.AddTag)
		//更新指定标签
		apiv1.PUT("/tags/:id", v1.EditTag)
		//删除指定标签
		apiv1.DELETE("/tags/:id", v1.DeleteTag)
	}

	//将本次注册的router返回,方便使用
	return r
}


检查路由是否注册成功

回到命令行,执行go run main.go,检查路由规则是否注册成功。

$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)

下载依赖包

首先我们要拉取validation的依赖包,在后面的接口里会使用到表单验证

$ go get -u github.com/astaxie/beego/validation

编写标签列表的 models 逻辑

创建models目录下的tag.go,写入文件内容:

package models

import (
	"github.com/jinzhu/gorm"
	"time"
)

// Tag 定义tag表的相关表头
type Tag struct {
	Model

	Name       string `json:"name"`
	CreatedBy  string `json:"created_by"`
	ModifiedBy string `json:"modified_by"`
	State      int    `json:"state"`
}

//以下两个函数都为命名返回,函数内部有相关定义

// GetTags page-size,每页显示的tag数	pageNum,即为从第几条记录开始显示,从0开始计数
func GetTags(pageNum int, pageSize int, maps interface{}) (tags []Tag) {

	// where查询使用map条件
	//db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
	// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

	db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags) //db在models.go中已经定义,同一个包可以直接调用
	return
}

// GetTagTotal 查询tag总数
func GetTagTotal(maps interface{}) (count int) {
	db.Model(&Tag{}).Where(maps).Count(&count)

	return
}
  1. 我们创建了一个Tag struct{},用于Gorm的使用。并给予了附属属性json,这样子在c.JSON的时候就会自动转换格式,非常的便利

  2. 可能会有的初学者看到return,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了

  3. 有人会疑惑db是哪里来的;因为在同个models包下,因此db *gorm.DB是可以直接使用的

编写标签列表的路由逻辑

打开routers目录下 v1 版本的tag.go,第一我们先编写获取标签列表的接口

修改文件内容:

package v1

import (
    "net/http"

    "github.com/gin-gonic/gin"
    //"github.com/astaxie/beego/validation"
    "github.com/Unknwon/com"

    "gin-blog/pkg/e"
    "gin-blog/models"
    "gin-blog/pkg/util"
    "gin-blog/pkg/setting"
)

// GetTags 获取多个文章标签
func GetTags(c *gin.Context) {
	//查询参数方法,及url中?name=xxx
	name := c.Query("name")

	maps := make(map[string]interface{})
	data := make(map[string]interface{})

	if name != "" {
		maps["name"] = name
	}

	var state int = -1
	if arg := c.Query("state"); arg != "" {
		state = com.StrTo(arg).MustInt()
		maps["state"] = state
	}

	code := e.SUCCESS //使用之前约定的错误码

	data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)
	data["total"] = models.GetTagTotal(maps)

	//gin.h是一种简便的返回json的方式
	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": data,
	})
}

//新增文章标签
func AddTag(c *gin.Context) {
}

//修改文章标签
func EditTag(c *gin.Context) {
}

//删除文章标签
func DeleteTag(c *gin.Context) {
}
  1. c.Query可用于获取?name=test&state=1这类 URL 参数,而c.DefaultQuery则支持设置一个默认值
  2. code变量使用了e模块的错误编码,这正是先前规划好的错误码,方便排错和识别记录
  3. util.GetPage保证了各接口的page处理是一致的
  4. c *gin.Context是Gin很重要的组成部分,可以理解为上下文,它允许我们在中间件之间传递变量、管理流、验证请求的 JSON 和呈现 JSON 响应

在本机执行curl 127.0.0.1:8000/api/v1/tags,正确的返回值为{"code":200,"data":{"lists":[],"total":0},"msg":"ok"},若存在问题请结合gin结果进行拍错。

在获取标签列表接口中,我们可以根据name、state、page来筛选查询条件,分页的步长可通过app.ini进行配置,以lists、total的组合返回达到分页效果。

编写新增标签的 models 逻辑

接下来我们编写新增标签的接口

打开models目录下的tag.go,修改文件(增加 2 个方法):

...
// ExistTagByName 根据name查询tag是否存在
func ExistTagByName(name string) bool {
	var tag Tag
	//查询词条
	db.Select("id").Where("name = ?", name).First(&tag)

	if tag.ID > 0 {
		return true
	}

	return false
}

// AddTag 创建新tag
func AddTag(name string, state int, createdBy string) bool {
	//在对应数据表中创建词条
	db.Create(&Tag{
		Name:      name,
		State:     state,
		CreatedBy: createdBy,
	})

	return true
}

...

编写新增标签的路由逻辑

打开routers目录下的tag.go,修改文件(变动 AddTag 方法):

package v1

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/astaxie/beego/validation"
    "github.com/Unknwon/com"

    "gin-blog/pkg/e"
    "gin-blog/models"
    "gin-blog/pkg/util"
    "gin-blog/pkg/setting"
)

...

// AddTag 新增文章标签
func AddTag(c *gin.Context) {

	//参数查询 url中name
	name := c.Query("name")
	//参数查询 state 这里设置默认为0
	state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
	//参数查询
	createdBy := c.Query("created_by")

	valid := validation.Validation{}
	valid.Required(name, "name").Message("名称不能为空")
	valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
	valid.Required(createdBy, "created_by").Message("创建人不能为空")
	valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
	valid.Range(state, 0, 1, "state").Message("状态只允许0或1")

	//运行到这里设置为 参数错误
	code := e.INVALID_PARAMS

	//将gin获得的数据与数据库作比较,进行验证
	if !valid.HasErrors() {
		if !models.ExistTagByName(name) {
			code = e.SUCCESS
			models.AddTag(name, state, createdBy)
		} else {
			code = e.ERROR_EXIST_TAG
		}
	}

	//json相应
	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}
...

PostmanPOST 访问http://127.0.0.1:8000/api/v1/tags?name=1&state=1&created_by=test,查看code是否返回200blog_tag表中是否有值,有值则正确。

编写 models callbacks

但是这个时候大家会发现,我明明新增了标签,但created_on居然没有值,那做修改标签的时候modified_on会不会也存在这个问题?

为了解决这个问题,我们需要打开models目录下的tag.go文件,修改文件内容(修改包引用和增加 2 个方法):

package models

import (
    "time"

    "github.com/jinzhu/gorm"
)

...

// BeforeCreate 建立hook钩子函数,在创建之前插入时间值
func (tag *Tag) BeforeCreate(scope *gorm.Scope) error {
	//我们在定义的时候createon是int类型,因此我们这里使用unix方法将时间转变为时间戳
	scope.SetColumn("CreatedOn", time.Now().Unix())

	return nil
}

// BeforeUpdate 同为hook钩子函数,更新的时候加入修改时间值
func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {
	scope.SetColumn("ModifiedOn", time.Now().Unix())

	return nil
}
...

重启服务,再在用PostmanPOST 访问http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test,发现created_on已经有值了!

这里涉及到gorm的相关方式,更多的可以查看这篇gormhook相关的文章

编写其余接口的路由逻辑

接下来,我们一口气把剩余的两个接口(EditTag、DeleteTag)完成吧

打开routers目录下 v1 版本的tag.go文件,修改内容:

...
// EditTag 修改文章标签
func EditTag(c *gin.Context) {
	//.param 动态参数查询,并将其确定转换为int
	id := com.StrTo(c.Param("id")).MustInt()

	//参数查询,查询对应key
	name := c.Query("name")
	modifiedBy := c.Query("modified_by")

	//设定验证信息
	valid := validation.Validation{}

	var state int = -1
	if arg := c.Query("state"); arg != "" {
		state = com.StrTo(arg).MustInt()
		valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
	}

	valid.Required(id, "id").Message("ID不能为空")
	valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
	valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
	valid.MaxSize(name, 100, "name").Message("名称最长为100字符")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		code = e.SUCCESS
		if models.ExistTagByID(id) {
			data := make(map[string]interface{})
			data["modified_by"] = modifiedBy
			if name != "" {
				data["name"] = name
			}
			if state != -1 {
				data["state"] = state
			}

			models.EditTag(id, data)
		} else {
			code = e.ERROR_NOT_EXIST_TAG
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}

// DeleteTag 删除文章标签
func DeleteTag(c *gin.Context) {
	//动态参数查询
	id := com.StrTo(c.Param("id")).MustInt()

	//验证信息
	valid := validation.Validation{}
	valid.Min(id, 1, "id").Message("ID必须大于0")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		code = e.SUCCESS
		if models.ExistTagByID(id) {
			models.DeleteTag(id)
		} else {
			code = e.ERROR_NOT_EXIST_TAG
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}

编写其余接口的 models 逻辑

打开models下的tag.go,修改文件内容:

...

// ExistTagByID 根据id查询表中tag是否存在
func ExistTagByID(id int) bool {
	var tag Tag //实例化tag

	db.Select("id").Where("id = ?", id).First(&tag)
	if tag.ID > 0 {
		return true
	}

	return false
}

// DeleteTag 根据id删除表中tag
func DeleteTag(id int) bool {
	db.Where("id = ?", id).Delete(&Tag{})

	return true
}

// EditTag 修改tag
func EditTag(id int, data interface{}) bool {
	db.Model(&Tag{}).Where("id = ?", id).Updates(data)

	return true
}

...

验证功能

重启服务,用 Postman

  • PUT 访问 http://127.0.0.1:8000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1 ,查看 code 是否返回 200
  • DELETE 访问 http://127.0.0.1:8000/api/v1/tags/1 ,查看 code 是否返回 200
    至此,TagAPI’s 完成,下一节我们将开始 ArticleAPI’s 编写!

完成博客的文章类接口定义和编写

定义接口

本节编写文章的逻辑,我们定义一下接口吧!

  • 获取文章列表:GET("/articles”)
  • 获取指定文章:POST("/articles/:id”)
  • 新建文章:POST("/articles”)
  • 更新指定文章:PUT("/articles/:id”)
  • 删除指定文章:DELETE("/articles/:id”)

编写路由空壳

routersv1 版本下,新建article.go文件,写入内容:

package v1

import (
    "github.com/gin-gonic/gin"
)

//获取单个文章
func GetArticle(c *gin.Context) {
}

//获取多个文章
func GetArticles(c *gin.Context) {
}

//新增文章
func AddArticle(c *gin.Context) {
}

//修改文章
func EditArticle(c *gin.Context) {
}

//删除文章
func DeleteArticle(c *gin.Context) {
}

我们打开routers下的router.go文件,修改文件内容为:

package routers

import (
    "github.com/gin-gonic/gin"
	
	"github.com/kingsill/gin-example/pkg/setting"
	"github.com/kingsill/gin-example/routers/api/v1"
)

func InitRouter() *gin.Engine {
    ...
    apiv1 := r.Group("/api/v1")
    {
        ...
        //获取文章列表
        apiv1.GET("/articles", v1.GetArticles)
        //获取指定文章
        apiv1.GET("/articles/:id", v1.GetArticle)
        //新建文章
        apiv1.POST("/articles", v1.AddArticle)
        //更新指定文章
        apiv1.PUT("/articles/:id", v1.EditArticle)
        //删除指定文章
        apiv1.DELETE("/articles/:id", v1.DeleteArticle)
    }

    return r
}

当前目录结构:

go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime

在基础的路由规则配置结束后,我们开始编写我们的接口吧!

编写models逻辑

创建models目录下的article.go,写入文件内容:

package models

import (
	"github.com/jinzhu/gorm"

	"time"
)

// Article 建立对应article表的struct结构体,方便进行信息读写
type Article struct {
	Model

	TagID int `json:"tag_id" gorm:"index"`
	Tag   Tag `json:"tag"`

	Title      string `json:"title"`
	Desc       string `json:"desc"`
	Content    string `json:"content"`
	CreatedBy  string `json:"created_by"`
	ModifiedBy string `json:"modified_by"`
	State      int    `json:"state"`
}

// BeforeCreate 与tag的逻辑相同 为了插入createOn时间戳
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
	scope.SetColumn("CreatedOn", time.Now().Unix())

	return nil
}

// BeforeUpdate 与tag的逻辑相同 是为了更新数据是插入modifiedOn数据
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
	scope.SetColumn("ModifiedOn", time.Now().Unix())

	return nil
}

我们创建了一个Article struct {},与Tag不同的是,Article多了几项,如下:

  • gorm:index,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响
  • Tag字段,实际是一个嵌套的struct,它利用TagIDTag模型相互关联,在执行查询的时候,能够达到Article、Tag关联查询的功能;这里实际上为一对一的连接,具体可以查看该篇文章,但是如果是一对一连接的话(?),一篇文章就只能对应一个标签。这里暂且放过,看后续实现
  • time.Now().Unix() 返回当前的时间戳
    接下来,请确保已对上一章节的内容通读且了解,由于逻辑偏差不会太远,我们本节直接编写这五个接口
逻辑接口
...
// ExistArticleByID 根据id查询文章是否存在
func ExistArticleByID(id int) bool {
	var article Article
	db.Select("id").Where("id = ?", id).First(&article)

	if article.ID > 0 {
		return true
	}

	return false
}

// GetArticleTotal 获取文章总数,使用时通过map传递限制参数
func GetArticleTotal(maps interface{}) (count int) {
	db.Model(&Article{}).Where(maps).Count(&count)

	return
}

// GetArticles 显示文章列表,分页显示
func GetArticles(pageNum int, pageSize int, maps interface{}) (articles []Article) {
	db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

	return
}

// GetArticle 通过id进行文章查询
func GetArticle(id int) (article Article) {
	db.Where("id = ?", id).First(&article)
	db.Model(&article).Related(&article.Tag)

	return
}

// EditArticle 更新文章信息
func EditArticle(id int, data interface{}) bool {
	db.Model(&Article{}).Where("id = ?", id).Updates(data)

	return true
}

// AddArticle 添加文章
func AddArticle(data map[string]interface{}) bool {
	db.Create(&Article{
		TagID:     data["tag_id"].(int),
		Title:     data["title"].(string),
		Desc:      data["desc"].(string),
		Content:   data["content"].(string),
		CreatedBy: data["created_by"].(string),
		State:     data["state"].(int),
	})

	return true
}

// DeleteArticle 根据id删除文章
func DeleteArticle(id int) bool {
	db.Where("id = ?", id).Delete(Article{})

	return true
}

...

这里用到了gorm的很多相关知识,尤其是这里使用了关联的方法,可以从gorm专栏中学习有关知识,或者从官方的中文文档查询

  1. 我们的Article是如何关联到Tag?
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)

    return
}

能够达到关联,首先是gorm本身做了大量的约定俗成

  • Article有一个结构体成员是TagID,就是外键。gorm会通过类名+ID 的方式去找到这两个类之间的关联关系
  • Article有一个结构体成员是Tag,就是我们嵌套在Article里的Tag结构体,我们可以通过Related进行关联查询
  1. Preload是什么东西,为什么查询可以得出每一项的关联Tag
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

    return
}

Preload就是一个预加载器,它会执行两条 SQL,分别是SELECT * FROM blog_articles;和SELECT * FROM blog_tag WHERE id IN (1,2,3,4);,那么在查询出结构后,gorm内部处理对应的映射逻辑,将其填充到ArticleTag中,会特别方便,并且避免了循环查询

  1. v.(I) 是什么?

v表示一个接口值,I表示接口类型。这个实际就是 Golang 中的类型断言,用于判断一个接口值的实际类型是否为某个类型,或一个非接口值的类型是否实现了某个接口类型

编写路由逻辑

打开routers目录下 v1 版本的article.go文件,修改文件内容:

package v1

import (
	"log"
	"net/http"

	"github.com/astaxie/beego/validation"
	"github.com/gin-gonic/gin"
	"github.com/unknwon/com"

	"github.com/kingsill/gin-example/models"
	"github.com/kingsill/gin-example/pkg/e"
	"github.com/kingsill/gin-example/pkg/setting"
	"github.com/kingsill/gin-example/pkg/util"
)

// GetArticle 获取单个文章
func GetArticle(c *gin.Context) {
	id := com.StrTo(c.Param("id")).MustInt()

	valid := validation.Validation{}
	valid.Min(id, 1, "id").Message("ID必须大于0")

	//定义初始的code
	code := e.INVALID_PARAMS

	var data interface{}

	if !valid.HasErrors() {
		if models.ExistArticleByID(id) {
			data = models.GetArticle(id)
			code = e.SUCCESS
		} else {
			code = e.ERROR_NOT_EXIST_ARTICLE
		}
	} else {
		for _, err := range valid.Errors {
			log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": data,
	})
}

// GetArticles 获取多个文章
func GetArticles(c *gin.Context) {
	data := make(map[string]interface{})
	maps := make(map[string]interface{})
	valid := validation.Validation{}

	var state int = -1
	if arg := c.Query("state"); arg != "" {
		state = com.StrTo(arg).MustInt()
		maps["state"] = state

		valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
	}

	var tagId int = -1
	if arg := c.Query("tag_id"); arg != "" {
		tagId = com.StrTo(arg).MustInt()
		maps["tag_id"] = tagId

		valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
	}

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		code = e.SUCCESS

		data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
		data["total"] = models.GetArticleTotal(maps)

	} else {
		for _, err := range valid.Errors {
			log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": data,
	})
}

// AddArticle 新增文章
func AddArticle(c *gin.Context) {
	tagId := com.StrTo(c.Query("tag_id")).MustInt()
	title := c.Query("title")
	desc := c.Query("desc")
	content := c.Query("content")
	createdBy := c.Query("created_by")
	state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()

	valid := validation.Validation{}
	valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
	valid.Required(title, "title").Message("标题不能为空")
	valid.Required(desc, "desc").Message("简述不能为空")
	valid.Required(content, "content").Message("内容不能为空")
	valid.Required(createdBy, "created_by").Message("创建人不能为空")
	valid.Range(state, 0, 1, "state").Message("状态只允许0或1")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		if models.ExistTagByID(tagId) {
			data := make(map[string]interface{})
			data["tag_id"] = tagId
			data["title"] = title
			data["desc"] = desc
			data["content"] = content
			data["created_by"] = createdBy
			data["state"] = state

			models.AddArticle(data)
			code = e.SUCCESS
		} else {
			code = e.ERROR_NOT_EXIST_TAG
		}
	} else {
		for _, err := range valid.Errors {
			log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]interface{}),
	})
}

// EditArticle 修改文章
func EditArticle(c *gin.Context) {
	valid := validation.Validation{}

	id := com.StrTo(c.Param("id")).MustInt()
	tagId := com.StrTo(c.Query("tag_id")).MustInt()
	title := c.Query("title")
	desc := c.Query("desc")
	content := c.Query("content")
	modifiedBy := c.Query("modified_by")

	var state int = -1
	if arg := c.Query("state"); arg != "" {
		state = com.StrTo(arg).MustInt()
		valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
	}

	valid.Min(id, 1, "id").Message("ID必须大于0")
	valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
	valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
	valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
	valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
	valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		if models.ExistArticleByID(id) {
			if models.ExistTagByID(tagId) {
				data := make(map[string]interface{})
				if tagId > 0 {
					data["tag_id"] = tagId
				}
				if title != "" {
					data["title"] = title
				}
				if desc != "" {
					data["desc"] = desc
				}
				if content != "" {
					data["content"] = content
				}

				data["modified_by"] = modifiedBy

				models.EditArticle(id, data)
				code = e.SUCCESS
			} else {
				code = e.ERROR_NOT_EXIST_TAG
			}
		} else {
			code = e.ERROR_NOT_EXIST_ARTICLE
		}
	} else {
		for _, err := range valid.Errors {
			log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}

// DeleteArticle 删除文章
func DeleteArticle(c *gin.Context) {
	id := com.StrTo(c.Param("id")).MustInt()

	valid := validation.Validation{}
	valid.Min(id, 1, "id").Message("ID必须大于0")

	code := e.INVALID_PARAMS
	if !valid.HasErrors() {
		if models.ExistArticleByID(id) {
			models.DeleteArticle(id)
			code = e.SUCCESS
		} else {
			code = e.ERROR_NOT_EXIST_ARTICLE
		}
	} else {
		for _, err := range valid.Errors {
			log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  e.GetMsg(code),
		"data": make(map[string]string),
	})
}

当前目录结构

go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── article.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime

验证功能

我们重启服务,执行go run main.go,检查控制台输出结果

$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> gin-blog/routers/api/v1.GetArticles (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> gin-blog/routers/api/v1.GetArticle (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> gin-blog/routers/api/v1.AddArticle (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> gin-blog/routers/api/v1.EditArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)

使用Postman检验接口是否正常,在这里大家可以选用合适的参数传递方式,此处为了方便展示我选用了GET/Param传参的方式,而后期会改为 POST

新建文章:
POST:http://127.0.0.1:8000/api/v1/articles?tag_id=1&title=test1&desc=test-desc&content=test-content&created_by=test-created&state=1

获取文章列表:
GET:http://127.0.0.1:8000/api/v1/articles

查询指定文章:
GET:http://127.0.0.1:8000/api/v1/articles/1

修改文章:
PUT:http://127.0.0.1:8000/api/v1/articles/1?tag_id=1&title=test-edit1&desc=test-desc-edit&content=test-content-edit&modified_by=test-created-edit&state=0

删除文章:
DELETE:http://127.0.0.1:8000/api/v1/articles/1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1365104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

React.Children.map 和 js 的 map 有什么区别?

JavaScript 中的 map 不会对为 null 或者 undefined 的数据进行处理&#xff0c;而 React.Children.map 中的 map 可以处理 React.Children 为 null 或者 undefined 的情况。 React 空节点&#xff1a;可以由null、undefined、false、true创建 import React from reactexport …

详解ajax、fetch、axios的区别

众所周知它们都用来发送请求&#xff0c;其实它们区别还蛮大的。这也是面试中的高频题&#xff0c;本文将详细进行讲解。 1. ajax 英译过来是Aysnchronous JavaScript And XML&#xff0c;直译是异步JS和XML&#xff08;XML类似HTML&#xff0c;但是设计宗旨就为了传输数据&a…

NI基于PC的测量和控制系统

基于PC的测量和控制系统为工程师提供了电气和物理测量功能&#xff0c;使其能够以可自定义、准确且经济实惠的方式进行台式测量. 什么是基于PC的测量和控制系统&#xff1f; 在基于PC的测量和控制系统中&#xff0c;NI硬件产品通过USB或以太网连接到PC或笔记本电脑。这种系统具…

在ultralytics中实现obb检测,官方团队做了哪些修改?

在最新的在ultralytics中,官方团队已经提供了obb检测功能,并发布了相应的预训练模型,其具体使用可以参考。https://blog.csdn.net/a486259/article/details/134557389 1、网络结构 1.1 OBB(Detect) 在ultralytics\nn\modules\head.py中,实现了OBB head,用于在网络head…

致远OA getAjaxDataServlet XXE漏洞复现(QVD-2023-30027)

0x01 产品简介 致远互联-OA 是数字化构建企业数字化协同运营中台,面向企业各种业务场景提供一站式大数据分析解决方案的协同办公软件。 0x02 漏洞概述 致远互联-OA getAjaxDataServlet 接口处存在XML实体注入漏洞,未经身份认证的攻击者可以利用此漏洞读取系统内部敏感文件…

Marvelous Designer 各版本安装指南

Marvelous Designer下载链接 https://pan.baidu.com/s/1ZZCraq6w2Z4JPisND8q0jA?pwd0531 1.鼠标右击【Marvelous Designer 12(64bit)】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;选择【解压到 Marvelous Designer 12(64bit)】。 2.打开解压后的…

软件测试|测试平台开发-Flask 入门:编写第一个简单 Web 应用

简介 Flask 是一个轻量级的 Python Web 框架&#xff0c;它使得创建 Web 应用变得简单快捷。相比于Django框架&#xff0c;它具有以下的优点&#xff1a; 轻&#xff1a;Flask 是一个轻量级的 Web 框架&#xff0c;使用 Python 语言编写易&#xff1a;较其他同类型框架更为灵…

关于burpsuite设置HTTP或者SOCKS代理

使用burpsuite给自己的浏览器做代理&#xff0c;抓包重发这些想必大家都清除 流量请求过程&#xff1a; 本机浏览器 -> burpsuite -> 目标服务器 实质还是本机发出的流量 如果我们想让流量由其他代理服务器发出 实现&#xff1a; 本机浏览器 -> burpsuite -> 某…

Linux程序、进程和计划任务

目录 一.程序和进程 1.程序的概念 2.进程的概念 3.线程的概念 4.单线程与多线程 5.进程的状态 二.查看进程信息相关命令&#xff1a; 1.ps&#xff1a;查看静态进程信息状态 2.top&#xff1a;查看动态进程排名信息 3.pgrep&#xff1a;查看指定进程 4.pstree&#…

图表分析网页模版 大数据可视化大屏电子沙盘合集

项目基于html/css/js&#xff0c;包含行业&#xff1a; 智慧政务 智慧社区 金融行业 智慧交通 智慧门店 智慧大厅 智慧物流 智慧医疗 通用模板 大数据分析平台 项目包含功能 (部分)&#xff1a; 实时数据K线图&#xff08;可自由配置多种行业模式&#xff09; 可切换式大屏展…

linux 01 centos镜像下载,服务器,vmware模拟服务器

https://www.bilibili.com/video/BV1pz4y1D73n?p3&vd_source4ba64cb9b5f8c56f1545096dfddf8822 01.使用的版本 国内主要使用的版本是centos 02.centos镜像下载 这里的是centos7 一.阿里云官网地址&#xff1a;https://www.aliyun.com/ 二. -----【文档与社区】 —【…

HCIA-Datacom题库(自己整理分类的)_11_其他网络协议单选【9道题】

1.DNS协议的主要作用是&#xff1f; 文件传输 远程接入 域名解析 邮件传输 2.下列属于链路状态协议的是? Direct static FTP OSPF 解析&#xff1a; FTP&#xff1a;文件传输协议 OSPF&#xff1a;链路状态路由协议 3.如下图所示的网络主机A通过Telnet登录到路由…

Java环境准备:JDK与IDEA

新手小白学Java–环境准备篇 文章目录 新手小白学Java--环境准备篇第1节 JDK的下载与安装第2节 IDEA的下载与安装第3节 使用IDEA创建第一个Java项目第4节 使用小技巧查看电脑的操作系统版本显示出文件的后缀名IDEA 修改字体大小IDEA 修改显示主题色IDEA 修改单行注释的颜色IDEA…

定时器@Scheduled使用

四种调度方法 Scheduled 是 Spring Boot 中用于创建定时任务的注解。使用此注解可以很方便地实现定时任务的自动调度。以下是常见四种参数的作用&#xff1a; 固定延迟&#xff08;Fixed Delay&#xff09;: Scheduled(fixedDelay 1000)&#xff1a;在上一个任务完成后&#…

STM32---中断

中断框图 一.中断 中断&#xff1a;当有中断请求时&#xff0c;CPU会停止处理当前的任务&#xff0c;转而去处理中断任务。 中断输入线有19/20根&#xff08;互联型号20根&#xff09;。 分类&#xff1a;系统异常&#xff08;10个&#xff09;和外部中断&#xff08;60个&…

thinkphp6入门(15)-- 模型动态构建查询条件

背景 我使用thinkphp6的模型写数据库查询&#xff0c;有多个where条件&#xff0c;但是不确定是否需要添加某个where条件&#xff0c;怎么才能动态得生成查询 链式查询 在ThinkPHP 6中&#xff0c;可以使用链式查询方法来动态地构建查询条件。可以根据参数的值来决定是否添加…

编译ZLMediaKit(win10+msvc2019_x64)

前言 因工作需要&#xff0c;需要ZLMediaKit&#xff0c;为方便抓包分析&#xff0c;最好在windows系统上测试&#xff0c;但使用自己编译的第三方库一直出问题&#xff0c;无法编译通过。本文档记录下win10上的编译过程&#xff0c;供有需要的小伙伴使用 一、需要安装的软件…

Halcon机器视觉和运动控制软件通用框架,24年1月最新版新增UI设计器,插件式开发,开箱即用 仅供学习!

24年1月更新 下载点我 此版本已经添加ui设计器。具体功能如上所示&#xff0c;可以自定义变量&#xff0c;写c#脚本&#xff0c;自定义流程&#xff0c;包含了halcon脚本和封装的算子&#xff0c;可自定义ui&#xff0c;通过插件形式开发很方便拓展自己的功能。 ui设计器

SpringMVC源码解析——HTTP请求处理

在SpringMVC源码解析——DispatcherServlet的逻辑处理中&#xff0c;最后介绍到了org.springframework.web.servlet.DispatcherServlet的doDispatch方法中关于处理Web HTTP请求的核心代码是调用AbstractHandlerMethodAdapter类的handle方法&#xff0c;源码如下&#xff1a; /*…

登录验证

会话技术 会话 打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开连接&#xff0c;会话结束。在一次会话中可以包含多次请求与响应 会话跟踪 一种维护浏览器的方法 服务器需要识别多次请求是否来自于同一浏览器 以便在同一次会话的多次…