本篇概要:
1. 开发环境、最简单的服务启动;
github 地址:https://github.com/gin-gonic/gin
# 创建模块文件夹
mkdir -p /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic
cd /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic
# 创建 go.mod
go mod init topic.gin.test.com
# 安装 gin,在当前目录执行
go get github.com/gin-gonic/gin
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
type Topic struct {
TopicID int
TopicTitle string
}
func main() {
// 赋值
//m := make(map[string]interface{})
//m["username"] = "huahua"
// 创建路由
router := gin.Default()
router.GET("/", func(context *gin.Context) {
// context.Writer.Write([]byte("hello"))
// context.JSON(http.StatusOK, m)
// context.JSON(http.StatusOK, gin.H{})
context.JSON(http.StatusOK, Topic{101, "话题"})
})
router.Run() // 默认 8080
}
2. API的URL规则设计、带参数的路由;
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
- 以下程序可执行,但不规范
- 目前不支持正则,也不支持固定路径和参数路径共存
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// GET /topic/{topic_id} 获取帖子明细
router.GET("/topic/:topic_id", func(c *gin.Context) {
c.String(http.StatusOK, "获取帖子id为:%s", c.Param("topic_id"))
})
// GET /topic/{user_name} 获取用户发布的帖子列表
// GET /topic/top 获取最热帖子列表
router.GET("/topic/top", func(c *gin.Context) {
c.String(http.StatusOK, "帖子列表:%s")
})
router.Run()
}
- 重新设计 api 有版本信息:/v1/topics
- 尽可能使用复数,且含义明确,名词最佳:/v1/topics,/v1/getusers 不推荐
- 使用GET参数规划数据展现规则
- /v1/users:显示全部或默认条数
- /v1/users?limit=10:只显示10条
- /v1/topics?username=hua,显示 hua 的帖子
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/v1/topics", func(c *gin.Context) {
// 默认参数设置,比如 abc 参数默认为 1
// c.DefaultQuery("abc",1)
if c.Query("username") == ""{
c.String(200,"获取帖子列表")
} else {
// /v1/topics?username=hua
c.String(200,"获取用户名=%s的帖子列表",c.Query("username"))
}
})
router.GET("/v1/topics/:topic_id", func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
})
router.Run()
}
3. 是否要用 MVC模式、路由分组;
常见的 MVC
实际开发:
- 重点业务重点部署(比如 User 相关 api)
- 路由分组,文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics")
{ // 代码块
v1.GET("", func(c *gin.Context) {
if c.Query("username")==""{
c.String(200,"获取帖子列表")
}else {
c.String(200,"获取用户名=%s的帖子列表",c.Query("username"))
}
})
v1.GET("/:topic_id", func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
})
}
router.Run()
}
4. 简单Dao层代码封装、使用中间件模拟"鉴权";
一些需求:
GET /v1/topics
默认显示所有话题列表GET /v1/topics?username=hua
显示用户发表的帖子GET /v1/topics/123
显示帖子 ID 为 123 的详细内容POST /v1/topics
外加JSON参数,即可进行帖子的新增 (是需要登录的)DELETE /v1/topics/123
删除帖子 (也要登录)- 简单的封装 POST /v1/topics?token=xxxxxx(判断登录)
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// 之后是处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
func NewTopic(c *gin.Context) {
//判断登录
c.String(200,"新增帖子")
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList() {
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics")
{ // 代码块
v1.GET("", func(c *gin.Context) {
if c.Query("username")==""{
c.String(200,"获取帖子列表")
}else {
c.String(200,"获取用户名=%s的帖子列表",c.Query("username"))
}
})
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
router.Run()
}
5. 创建Model、参数绑定 Model 的初步使用;
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
type Topic struct {
TopicID int `json:"id"` // 映射
TopicTitle string `json:"title"`
}
// 创建实体
func CreateTopic(id int, title string) Topic {
return Topic{id, title}
}
// 参数绑定
type TopicQuery struct {
UserName string `json:"username" form:"username"`
Page int `json:"page" form:"page" binding:"required"` // binding 必须有
PageSize int `json:"pagesize" form:"pagesize"`
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// 之后是处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
c.JSON(200, CreateTopic(101, "帖子标题"))
}
func NewTopic(c *gin.Context) {
//判断登录
c.String(200,"新增帖子")
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
//if c.Query("username")==""{
// c.String(200,"获取帖子列表")
//}else {
// c.String(200,"获取用户名=%s的帖子列表",c.Query("username"))
//}
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics")
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
router.Run()
}
6. 内置验证器的初步使用、POST参数绑定;
type Topic struct {
TopicID int `json:"id"`
TopicTitle string `json:"title" binding:"required" `
}
// 验证器来源于一个第三方库
// https://github.com/go-playground/validator
// 文档
// https://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
type Topic struct {
//TopicID int `json:"id"` // 映射
//TopicTitle string `json:"title" binding:"required"` // require 必须传参
TopicID int `json:"id"`
TopicTitle string `json:"title" binding:"min=4,max=20"` // 最小4,最大20字符
TopicShortTitle string `json:"stitle" binding:"required,nefield=TopicTitle"` // 短标题,nefield(not equal),和xx字段不能相等
UserIP string `json:"ip" binding:"ipv4"`
TopicScore int `json:"score" binding:"omitempty,gt=5"` // 帖子积分,omitempty 可空,填了必须大于5
}
// 创建实体
func CreateTopic(id int, title string) Topic {
// return Topic{id, title} // 只有两个字段,多字段建议写字段
return Topic{TopicID:id, TopicTitle:title}
}
// 参数绑定
type TopicQuery struct {
UserName string `json:"username" form:"username"`
Page int `json:"page" form:"page" binding:"required"` // binding 必须有
PageSize int `json:"pagesize" form:"pagesize"`
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// 之后是处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
c.JSON(200, CreateTopic(101, "帖子标题"))
}
//
func NewTopic(c *gin.Context) {
//判断登录
// c.String(200,"新增帖子")
// post http://localhost:8080/v1/topics?token=123
//{
// "title":"帖子标题",
// "stitle" : "小标题",
// "ip": "192.168.2.1",
// "score": 6
//}
topic:=Topic{}
err:=c.BindJSON(&topic) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topic)
}
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
//if c.Query("username")==""{
// c.String(200,"获取帖子列表")
//}else {
// c.String(200,"获取用户名=%s的帖子列表",c.Query("username"))
//}
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics")
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
router.Run()
}
7. 自定义验证器结合正则验证 JSON 参数;
8. 批量提交帖子数据的验证;
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
// 单个 Topic 实体
type Topic struct {
//TopicID int `json:"id"` // 映射
//TopicTitle string `json:"title" binding:"required"` // require 必须传参
TopicID int `json:"id"`
TopicTitle string `json:"title" binding:"min=4,max=20"` // 最小4,最大20字符
TopicShortTitle string `json:"stitle" binding:"required,nefield=TopicTitle"` // 短标题,nefield(not equal),和xx字段不能相等
UserIP string `json:"ip" binding:"ipv4"`
TopicScore int `json:"score" binding:"omitempty,gt=5"` // 帖子积分,omitempty 可空,填了必须大于5
}
// 多条
type Topics struct {
TopicList []Topic `json:"topics" binding:"gt=0,lt=3,dive"` // 切片,dive进一步验证单条实体
TopicListSize int `json:"size"`
}
// 创建实体
func CreateTopic(id int, title string) Topic {
// return Topic{id, title} // 只有两个字段,多字段建议写字段
return Topic{TopicID:id, TopicTitle:title}
}
// 参数绑定
type TopicQuery struct {
UserName string `json:"username" form:"username"`
Page int `json:"page" form:"page" binding:"required"` // binding 必须有
PageSize int `json:"pagesize" form:"pagesize"`
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// 之后是处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
c.JSON(200, CreateTopic(101, "帖子标题"))
}
// 单帖新增
func NewTopic(c *gin.Context) {
//判断登录
topic:=Topic{}
err:=c.BindJSON(&topic) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topic)
}
}
// 多帖批量新增
func NewTopics(c *gin.Context) {
//判断登录
topics:=Topics{}
err:=c.BindJSON(&topics) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topics)
}
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics") // 单条帖子
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
// 多条帖子
v2:=router.Group("/v1/mtopics")
{
v2.Use(MustLogin())
{
v2.POST("",NewTopics)
}
}
router.Run()
}
9. ORM、Gorm入手、执行原始 SQL;
# 驱动:https://github.com/go-sql-driver/mysql
# 引入Gorm
#https://github.com/jinzhu/gorm
# 文档:
# http://gorm.io/
# https://gorm.io/docs/
# 安装
# 在项目目录下执行
go get -u github.com/go-sql-driver/mysql
go get -u github.com/jinzhu/gorm
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
rows, _ := db.Raw("select topic_id, topic_title from topics").Rows()
for rows.Next() {
var t_id int
var t_title string
rows.Scan(&t_id, &t_title)
fmt.Println(t_id, t_title)
}
}
10. 结合 Model 进行数据映射、查询;
模型操作:https://gorm.io/docs/models.html
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
type TopicClass struct {
ClassId int `gorm:"primaryKey"` // 主键
ClassName string
ClassRemark string
ClassType string `gorm:"Column:classtype"` //指定字段名
}
文件 /Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
. "topic.gin.test.com/src"
)
func main() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
//NamingStrategy: schema.NamingStrategy{
// SingularTable: true, // 使用单数表名
//},
})
// 查询一条数据
//tc := &TopicClass{} // 引用不加"." 就 src.TopicClass{}
//db.Table("topic_class").First(&tc,2)
//fmt.Println(tc)
// 查询一堆数据
var tcs []TopicClass
// db.Table("topic_class").Where("classtype=?", 1).Find(&tcs)
db.Table("topic_class").Where(&TopicClass{ClassType:"1"}).Find(&tcs)
fmt.Println(tcs)
}