Gin、Gorm框架学习丨青训营笔记

69 阅读20分钟

Gin

简单介绍

  • gin是一个轻量级的go web框架,具有强大的功能
  1. 支持中间件,可以用来身份验证,记录日志信息等
  2. 路由组,可以更好地组织路由,是否需要授权,不同的API版本
  3. JSON解析,可以解析并验证请求的JSON

示例

安装、导入

go mod init demo //go mod init projectname
go mod tidy

go run test.go 

第一个gin应用

package main

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

)

func main() {
    // 初始化Gin路由器
    r := gin.Default() 

    // 为路由器r添加一个新的GET路由。当访问/hello URL时执行关联的函数
    r.GET("/hello", func(c *gin.Context) {
        // 使用c.JSON返回JSON格式的响应。第一个参数是HTTP状态码,第二个参数是要返回的数据
        c.JSON(http.StatusOK, "hello, world")
    })

    // 运行HTTP服务器,监听8080端口
    r.Run(":8080")
}
  • gin.Context:上下文,主要是处理请求、返回响应

具体使用

获取参数

queryString参数

func main() {
    r := gin.Default()

    r.GET("/hello", func(c * gin.Context) {
        name := c.DefaultQuery("name", "user1") //没有获取到参数会赋默认值
        age := c.Query("age")
        c.JSON(http.StatusOK, gin.H{
            "name": name,
            "age":  age,
        })
    })

    r.Run(":8080")
}
form参数
  • poastman:

func main() {
    r := gin.Default()

    r.GET("/hello", func(c * gin.Context) {
        name := c.DefaultPostForm("name", "user1") //没有获取到参数会赋默认值
        age := c.PostForm("age")
        c.JSON(http.StatusOK, gin.H{
            "name": name,
            "age":  age,
        })
    })

    r.Run(":8080")
}
uri(path)路径参数
func main() {
    r := gin.Default()

    r.GET("/hello/:name/:age", func(c * gin.Context) {
        name := c.Param("name")
        age := c.Param("age")
        c.JSON(http.StatusOK, gin.H{
            "name": name,
            "age":  age,
        })
    })

    r.Run(":8080")
}
参数绑定
  • 为了提高开发效率,使用gin的ShouldBind()。shouldBind()方法会根据请求中的contentType类型,将请求的参数自动绑定

  • 需要在结构体中声明:

    • form:"传递参数名":例如:user=xxx&password=123,
    • json:"传递参数名":例如:
    •   {
            "user": "test",
            "password": "123"
        }
      
package main

import (
    "net/http"

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

type loginUser struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

func main() {
    r := gin.Default()

    // 绑定JSON参数 {"user": "user1", "password": "123"}
    r.POST("/loginByJSON", func(c * gin.Context) {
        var login loginUser
        if err := c.ShouldBindJSON( & login) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "user":     login.User,
            "password": login.Password,
        })

    })

    //绑定Form参数 user=user1&password=123
    r.POST("/loginByForm", func(c * gin.Context) {
        var login loginUser
        if err := c.ShouldBind( & login) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "user":     login.User,
            "password": login.Password,
        })

    })

    //绑定QueryString参数同理
    
    r.Run(":8080")
}

路由

重定向

网址重定向
func main() {
    r := gin.Default()
    
    r.GET("/test", func(c * gin.Context) {
        //// 发送一个HTTP 301永久重定向响应,重定向到"http://www.baidu.com"
        c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
    })

    r.Run(":8080")
}
路由重定向
func main() {
    r := gin.Default()

    r.GET("/test1", func(c * gin.Context) {
        c.Request.URL.Path = "/test2"
        r.HandleContext(c) // 将修改后的上下文传递给路由器以处理新路径
    })

    r.GET("/test2", func(c * gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World",
        })
    })

    r.Run(":8080")
}

路由组

func main() {
    r := gin.Default()

    test := r.Group("/test") //大括号必须换行
    {
        test.POST("/login", func(c * gin.Context) {
        })

        test.POST("/register", func(c * gin.Context) {
        })
    }
    
    r.Run(":8080")
}

中间件

  • gin框架可以在处理请求的过程中,加入自己的钩子函数。这个钩子函数就叫中间件。用于处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等
  • gin的中间件必须是一个gin.HandlerFunc类型
package main  import (  "fmt"   "net/http"    "github.com/gin-gonic/gin"  )  func  f1 () gin.HandlerFunc {  return  func ( c   *  gin.Context) { fmt.Println("f1 start") c.Next() //继续处理 fmt.Println("f1 end") } }  func f2() gin.HandlerFunc {  return  func ( c   *  gin.Context) { fmt.Println("f2 start") c.Abort() //终止后续处理 fmt.Println("f2 end") } }  func f3() gin.HandlerFunc {  return  func ( c   *  gin.Context) { fmt.Println("f3 start") c.Next() fmt.Println("f3 end") } }  func main() { r  := gin.Default()  r.GET("/hello", f1(), f2(), f3(), func ( c   *  gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "success", }) })  r.Run(":8080") } 
  • 配置全局中间件
func  main () { r  := gin.Default()
    r.Use(f1()) //Use(中间件)即可  r.GET( "/hello" , f2(), f3(), func ( c   *  gin.Context) { c.JSON(http.StatusOK, gin.H{  "message" : "success" , }) })  r.Run( ":8080" ) } 

data/sql

连接MySQL

  • 在Go语言中,使用MySQL进行增删改查操作,通常需要通过一个数据库驱动,比如go-sql-driver/mysql,并结合database/sql标准库来完成。以下是一个简单的示例,使用Go语言对MySQL进行增删改查。
package main

import (
    "database/sql"
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql"
)

var db * sql.DB

func initDB() (err error) {
    //更改为自己设置的mysql密码和数据库,用户:密码
    dsn := "root:123456@tcp(localhost:3306)/demo1?charset=utf8mb4&parseTime=True&loc=Local" 
    db, err = sql.Open("mysql", dsn)
    if err != nil {
        return err
    }

    err = db.Ping()
    if err != nil {
        return err
    }

    // db.SetMaxOpenConns(1024) //   设置连接数总数
    // db.SetMaxIdleConns(100)  //  设置最大空闲连接数
    // db.SetConnMaxLifetime(0) // 设置连接最大生命周期, 默认为 0

    return
}

func main() {
    if err := initDB() ; err != nil {
        fmt.Println("connect failed:", err)
        panic(err)
    }

    defer db.Close()

    fmt.Println("connect success")
}

建表

  • 导入以下sql
/*
 Navicat Premium Data Transfer

 Source Server         : demo
 Source Server Type    : MySQL
 Source Server Version : 80039 (8.0.39)
 Source Host           : localhost:3306
 Source Schema         : demo1

 Target Server Type    : MySQL
 Target Server Version : 80039 (8.0.39)
 File Encoding         : 65001

 Date: 11/10/2024 16:02:24
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `account` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `type` tinyint NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `age` int NOT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'user1', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Mo Zhiyuan', 23);
INSERT INTO `user` VALUES (2, 'user2', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Fu Zhennan', 42);
INSERT INTO `user` VALUES (3, 'user3', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Du Zhennan', 72);
INSERT INTO `user` VALUES (4, 'user4', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Lok Kwok Ming', 87);
INSERT INTO `user` VALUES (5, 'user5', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Hashimoto Yuto', 39);
INSERT INTO `user` VALUES (6, 'user6', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Troy Ruiz', 14);
INSERT INTO `user` VALUES (7, 'user7', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Pan Rui', 76);
INSERT INTO `user` VALUES (8, 'user8', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Shao Yunxi', 99);
INSERT INTO `user` VALUES (9, 'user9', 'E10ADC3949BA59ABBE56E057F20F883E', 1, 'Heung Kwok Kuen', 48);
INSERT INTO `user` VALUES (10, 'admin', 'E10ADC3949BA59ABBE56E057F20F883E', 2, 'Che Ching Wan', 54);

SET FOREIGN_KEY_CHECKS = 1;
  • 密码均为123456!!!

查询

用法

  • 查询sql:SELECT 列名 FROM 表名
  • Query(), QueryRow
Query() //查询多条数据
QueryRow() //查询一条数据

//rows代表查询结果集, sql即需要的sql语句, param为sql查询参数
rows, err := db.Query(sql, param) 
if err != nil {
    return nil, err
}

defer rows.Close()

//遍历结果集
for rows.Next() {
    rows.Scan() 
}

练习

接口

API:http://localhost:8080/user/{userId}

Method:GET

参数形式:url

参数字段:

参数名称参数说明是否必填参数类型
userId用户idyesstring
  • 请求示例

http://localhost:8080/user/1

  • 返回值
{
    "data": {
        "UserId": 1,
        "Account": "user1",
        "Name": "Mo Zhiyuan",
        "Age": 23,
        "Type": 1
    }
}
参考代码
r.GET("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        var user User

        rows, err := db.Query(`
            SELECT user_id, account, name, age, type
            FROM user
            WHERE user_id = ?`, userId)
        if err != nil {
            panic(err)
        }

        defer rows.Close()

        for rows.Next() {
            err := rows.Scan( & user.UserId, & user.Account, & user.Name, & user.Age, & user.Type)
            if err != nil {
                panic(err)
            }
        }

        if user.UserId == 0 {
            c.JSON(http.StatusOK, gin.H{"msg": "用户不存在"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"data": user})
    })

创建

用法

  • 插入sql:INSERT INTO 表名(列名)VALUES(值)
_, err := db.Exec(sql, value)

练习

接口

Tip:需要对密码进行加密,不能将明文存储在数据库中

hash : = md5.Sum([]byte(user.Password))
npwd : = hex.EncodeToString(hash[:])

API: http://localhost:8080/user

Method: POST

参数形式:Body (application/json)

参数字段

参数名称参数说明是否必填参数类型
account用户账号yesstring
password用户密码yesstring
type用户类型yesint
name用户姓名yesstring
age用户年龄yesint

请求示例:

http://localhost:8080/user

{
    "account": "user9",
    "password": "123456",
    "type": 1,
    "name": "qsfga",
    "age": 10
}
参考代码
r.POST("/user", func(c * gin.Context) {
        var user User
        if err := c.ShouldBind( & user) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        hash := md5.Sum([]byte(user.Password))
        npwd := hex.EncodeToString(hash[:])

        _, err := db.Exec(`
            INSERT INTO user(account, password, name, age, type) 
            VALUES(?, ?, ?, ?, ?)`, user.Account, npwd, user.Name, user.Age, user.Type)

        if err != nil {
            panic(err)
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
    })

更新

用法

  • 更新sql:UPDATE 表名 SET 列名=
_, err := db.Exec(sql, value)

练习

接口

API: http://localhost:8080/user/{userId}

Method: PUT

参数形式:Body (application/json)

参数字段

参数名称参数说明是否必填参数类型
userId用户idyesstring
password用户密码yesstring
name用户姓名yesstring
age用户年龄yesint

请求示例:

http://localhost:8080/user/1

{
    "password": "123456",
    "name": "xxx1",
    "age": 20
}
参考代码
r.PUT("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        var user User
        if err := c.ShouldBind( & user) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        hash := md5.Sum([]byte(user.Password))
        npwd := hex.EncodeToString(hash[:])

        _, err := db.Exec(`
            UPDATE user
            SET password = ?, name = ?, age = ?
            WHERE user_id = ?`, npwd, user.Name, user.Age, userId)
        if err != nil {
            panic(err)
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
    })

删除

用法

  • 删除sql:DELETE FROM 表名
_, err := db.Exec(sql, value)

练习

接口

API: http://localhost:8080/user/{userId}

Method: DELETE

参数形式:url

参数字段

参数名称参数说明是否必填参数类型
userId用户idyesstring
参考代码
r.DELETE("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        _, err := db.Exec(`
            DELETE FROM user
            WHERE user_id = ?`, userId)
        if err != nil {
            panic(err)
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
})

自主练习(登录)

接口

API: http://localhost:8080/login

Method: POST

参数形式:Body (application/json)

参数字段:

参数名称参数说明是否必填参数类型说明
account账户yesstring
password密码yesstring需要md5加密
  • md5加密代码
hash := md5.Sum([]byte(str)) //替换str为密码即可
npwd := hex.EncodeToString(hash[:])
  • 请求示例
{
    "account": "user1",
    "password": "123456"
}
  • 返回值
{
    "data": {
        "UserId": 1,
        "Account": "user1",
        "Name": "Mo Zhiyuan",
        "Age": 23,
        "Type": 1
    }
}
  • 参考代码
package main  import (  "crypto/md5"   "database/sql"   "encoding/hex"   "fmt"   "net/http"    "github.com/gin-gonic/gin"  _ "github.com/go-sql-driver/mysql"  )  var db  *  sql.DB  type LoginUser struct { Account  string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` }  type User struct { UserId   int    `json:"UserId"` Account  string `json:"Account"` Password string `json:"Password"` Name     string `json:"Name"` Age      int    `json:"Age"` Type     int    `json:"Type"` }  func initDB() ( err error) { ds  := "root:1234@tcp(localhost:3306)/demo1" db, err = sql.Open("mysql", ds)  if err  != nil {  return err }  err = db.Ping()  if err  != nil {  return err }  // db.SetMaxOpenConns(1024) //   设置连接数总数 // db.SetMaxIdleConns(100)  //  设置最大空闲连接数 // db.SetConnMaxLifetime(0) // 设置连接最大生命周期, 默认为 0   return  }  func main() {  if err  := initDB()  ;  err  != nil { fmt.Println("connect failed:", err) panic(err) }   defer db.Close()  fmt.Println("connect success")  r  := gin.Default() r.POST("/login", func ( c   *  gin.Context) { var loginUser LoginUser  if err  := c.ShouldBind(  &  loginUser)  ;  err  != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})  return  }  var user User  //md5加密 hash  := md5.Sum([]byte(loginUser.Password)) npwd  := hex.EncodeToString(hash[:])  err  := db.QueryRow(` SELECT user_id, account, name, age, type FROM user WHERE account = ? AND password = ?`, loginUser.Account, npwd).Scan(  &  user.UserId,  &  user.Account,  &  user.Name,  &  user.Age,  &  user.Type)  if err  != nil { fmt.Println(err) panic(err) }   if user.UserId == 0 { c.JSON(http.StatusOK, gin.H{"msg": "用户名或密码错误"})  return  }  c.JSON(http.StatusOK, gin.H{"data": user}) })  r.Run(":8080") } 

Gorm

  • ORM(Object-Relational Mapping)对象关系映射,用于将对象模型与关系型数据库模型之间进行转换。从而让开发者不需要直接编写SQL语句操作数据库。
  • GORM是Go语言中非常流行的ORM框架。

连接MySQL

package main  import (     "fmt"     "net/http"     "gorm.io/driver/mysql"     "gorm.io/gorm" )  var db *gorm.DB  func initDB() (err error) {     dsn := "root:1234@tcp(localhost:3306)/demo1"     db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})     if err != nil {         return err     }      db = db.Debug()         return }  func main() {     if err := initDB(); err != nil {         fmt.Println("connect failed:", err)         panic(err)     }      fmt.Println("connect success") }

查询

用法

根据主键查询
db.First(&user, 10) 
//SELECT * FROM user WHERE id = 10

db.First(&user, "10") 
//SELECT * FROM user WHERE id = 10

db.Find(&users, []int{1, 2, 3}) 
//SELECT * FROM users WHERE id in (1, 2, 3);
查询全部对象
db.Find(&users) //SELECT * FROM users;
查询条件
string
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
Struct & Map
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
内联
// Get by primary key if it were a non-integer type
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;
not
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;

// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
Or
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
排序
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{
  Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
Limit、offest
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
Group by、having
type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
defer rows.Close()
for rows.Next() {
  ...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
defer rows.Close()
for rows.Next() {
  ...
}

type Result struct {
  Date  time.Time
  Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)
Distinct
db.Distinct("name", "age").Order("name, age desc").Find(&results)
Joins
type result struct {
  Name  string
  Email string
}

db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
  ...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

练习

接口

API:http:localhost:8080/user/{userId}

Method:GET

参数形式:url

参数字段:

参数名称参数说明是否必填参数类型
userId用户idyesstring
  • 请求示例

http://localhost:8080/user/1

  • 返回值
{
    "data": {
        "UserId": 1,
        "Account": "user1",
        "Name": "Mo Zhiyuan",
        "Age": 23,
        "Type": 1
    }
}
参考代码
r.GET("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        var user User

        if err := db.Find( & user, userId).Error ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if user.UserId == 0 {
            c.JSON(http.StatusOK, gin.H{"data": "用户不存在"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"data": user})
    })

创建

用法

所有字段创建
db.Save(&user)
选定字段创建
  • 选定字段创建:Select
  • 忽略字段创建:Omit
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")
批量插入
  • 传入切片
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)

for _, user := range users {
  user.ID // 1,2,3
}

练习

接口

API: http://localhost:8080/user

Method: POST

参数形式:Body (application/json)

参数字段

参数名称参数说明是否必填参数类型
account用户账号yesstring
password用户密码yesstring
name用户姓名yesstring
age用户年龄yesint

请求示例:

http://localhost:8080/user

{
    "account": "user9",
    "password": "123456",
    "name": "qsfga",
    "age": 10
}
参考代码
r.POST("/user", func(c * gin.Context) {
        var user User
        if err := c.ShouldBind( & user) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        hash := md5.Sum([]byte(user.Password))
        user.Password = hex.EncodeToString(hash[:])
        
        user.Type = 1
        
        if err := db.Create( & user).Error ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
    })

更新

用法

更新所有字段
  • 保存所有的字段,即使字段是零值
  • 如果保存值不包含主键,会执行create,否则执行update
db.Save(&user)

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1
更新单列
// 根据条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User 的 ID 是 `111`
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
更新多列
// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
更新选定字段
// 选择 Map 的字段
// User 的 ID 是 `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 选择 Struct 的字段(会选中零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// 选择所有字段(选择包括零值字段的所有字段)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

// 选择除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

练习

接口

API: http://localhost:8080/user/{userId}

Method: PUT

参数形式:Body (application/json)

参数字段

参数名称参数说明是否必填参数类型
userId用户idyesstring
password用户密码yesstring
name用户姓名yesstring
age用户年龄yesint

请求示例:

http://localhost:8080/user/1

{
    "password": "123456",
    "name": "xxx1",
    "age": 20
}
参考代码
r.PUT("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        var reqUser User
        var user User

        if err := c.ShouldBind( & reqUser) ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if err := db.Find( & user, userId).Error ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        hash := md5.Sum([]byte(reqUser.Password))
        user.Password = hex.EncodeToString(hash[:])

        user.Name = reqUser.Name
        user.Age = reqUser.Age

        if err := db.Save( & user).Error ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
    })

删除

用法

根据主键删除
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
批量删除
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

练习

接口

API: http://localhost:8080/user/{userId}

Method: DELETE

参数形式:url

参数字段

参数名称参数说明是否必填参数类型
userId用户idyesstring
参考代码
r.DELETE("/user/:userId", func(c * gin.Context) {
        userId := c.Param("userId")

        if err := db.Delete( & User{}, userId).Error ; err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"msg": "success"})
})

跨域问题

  • 浏览器的同源策略:两个URL的协议、域名和端口都必须相同,不然默认不允许发起请求。
当前页面url被请求页面url是否跨域原因
www.test.com/www.test.com/index.html协议,域名,端口相同
www.test.com/www.test.com/index.html协议不同
www.test.com/www.baidu.com/主域名不同
www.test.com/test1.test.com/子域名不同
www.test.com:8080/www.test.com:8000/端口号不同
  • CORS是跨域资源分享。只需服务器设置Access-Control-Allow-Origin
func CORSMiddleware() gin.HandlerFunc {
    return func(c * gin.Context) {
        method : = c.Request.Method
        c.Header("Access-Control-Allow-Origin", c.GetHeader("Origin"))
        c.Header("Access-Control-Allow-Credentials", "true")

        if method == "OPTIONS" {
            c.Header("Access-Control-Allow-Methods", c.GetHeader("Access-Control-Request-Method"))
            c.Header("Access-Control-Allow-Headers", c.GetHeader("Access-Control-Request-Headers"))
            c.Header("Access-Control-Max-Age", "7200")
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        c.Next()
    }
}

身份认证

  • 身份认证(俗称鉴权)是每个网站必不可少的

  • 使用session的流程

    • 用户使用账号密码登录
    • 服务器验证通过后会在session中保存相关数据,并向用户返回一个session_id,写在用户的cookie中
    • 之后用户每次请求都会通过cookie将session_id传回服务器
    • 服务器根据session_id查询用户的数据
  • 优点:安全性较高

  • 缺点:扩展性不好,不利于集群部署

JWT

  • JWT全称JSON Web Token。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJVc2VySWQiOjIsIlVzZXJUeXBlIjoxLCJleHAiOjE3MzY3MzYxNjksIm5iZiI6MTcyODk2MDE2OSwiaWF0IjoxNzI4OTYwMTY5fQ.
h-nTtfYj04FMLHZTh-R0gaZYCMaCoDu8wV2pE3ZcBk8
  • 分为三个部分,header,payload,signature。header和payload以JSON形式存在,每部分都分别经过Base64编码,以.拼接成一个JWT Token。

Header

  • header是一个JSON对象,描述JWT的元数据,alg表示签名的算法,默认是HMAC SHA256,type表示令牌的类型。最后将这个JSON对象用Base64URL算法转换成字符串
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

  • payload是一个JSON对象,用来存放实际需要的数据。JWT规定7个官方字段
Issuer:签发人
ExpiresAt:过期时间
Subject:主题
Audience:受众
NotBefore:生效时间
IssuedAt:签发时间
ID:编号
  • 除了这些官方字段外,你也可以指定字段和内容
  • 注意JWT默认是不加密的,任何人都可以读到,不要将秘密信息放在这里

Signature

  • signature是对前两部分的签名,防止数据篡改。需要指定一个密钥,然后使用签名算法

Base64URL

  • Base64是一种编码方式,用于将二进制数据转换为文本格式。
  • 而Base64URL与Base64的区别是会将+、/和=替换掉,=被省略,+替换成-,/替换成_,因为jwt可以会被放在url中+、/、=在URL里有特殊含义

JWT的使用

  • 客户端收到服务器返回的JWT,可以存储在cookie里,也可以存储在localStorage。
  • 客户端每次发送请求都需要带上JWT,最好的做法是放在HTTP请求头的Authorization字段里
Authorization: token
  • postaman使用方法

Go中使用
  • 定义结构体
//自定义结构体,嵌入了jwt.RegisteredClaims结构体
type MyClaims struct {
    UserId   int
    UserType int
    jwt.RegisteredClaims
}
  • 生成jwt对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, MyClaims{
        UserId:   userId,
        UserType: userType,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expiresAt),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "",
            Subject:   "",
        },
    })
  • 签名
token.SignedString([]byte(secretKey))
  • 解析jwt
func ParseToken(tokenString string, secreKey string) ( * MyClaims, error) {
    //解析tokenstring根据签名的密钥,三个参数:
    //tokenString表示要解析的jwt字符串,
    //&MyClaims{}:用于指定jwt的声明类型,解析后的声明会存储到这个结构体中
    //函数,接收一个jwt令牌,并返回用于签名的密钥
    token, err := jwt.ParseWithClaims(tokenString, & MyClaims{}, func(token * jwt.Token) (interface{}, error) {
        return []byte(secreKey), nil
    })
    if err != nil {
        return nil, err
    }
    
    //token进行类型断言,检查是否是MyCalims类型,token.valid检查token是否在有效期内且签名正确
    if claims, ok := token.Claims.( * MyClaims) ; ok && token.Valid {
        return claims, nil
    } else {
        return nil, errors.New("invalid token")
    }
}
  • 完整代码
package jwt

import (
    "errors"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

type MyClaims struct {
    UserId   int
    UserType int
    jwt.RegisteredClaims
}

func GetToken(userId int, userType int, secretKey string, expiresAt time.Time) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, MyClaims{
        UserId:   userId,
        UserType: userType,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expiresAt),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "",
            Subject:   "",
        },
    })

    return token.SignedString([]byte(secretKey))
}

func ParseToken(tokenString string, secreKey string) ( * MyClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, & MyClaims{}, func(token * jwt.Token) (interface{}, error) {
        return []byte(secreKey), nil
    })
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.( * MyClaims) ; ok && token.Valid {
        return claims, nil
    } else {
        return nil, errors.New("invalid token")
    }
}

优缺点

JWT最大优势是不需要存储Session。使得服务器鉴权变得简单,但是由于有效期存储在Token中,JWT 一旦签发,在有效期内一直可用,无法在服务器禁止。比如需要禁用用户,单纯使用JWT就无法做到实时了。