分类 分享 下的文章

什么是异步操作?

异步操作(Asynchronous Operation)是指程序在执行某个操作时,不需要等待该操作完成就能继续执行后续代码的编程模式。与同步操作(必须等待操作完成才能继续)不同,异步操作允许程序在等待某些耗时操作(如I/O、网络请求等)完成的同时继续执行其他任务。

异步操作的核心特点

  1. 非阻塞:主线程不会被阻塞,可以继续执行其他任务
  2. 延迟处理:操作结果将在未来某个时间点可用
  3. 回调机制:通常通过回调函数、Promise或async/await来处理结果
  4. 提高效率:特别适合I/O密集型操作,能更好地利用系统资源

同步 vs 异步

同步操作示例(阻塞式)

// 同步读取文件(Node.js)
const fs = require('fs');
const data = fs.readFileSync('file.txt'); // 程序会停在这里直到文件读取完成
console.log(data);
console.log('这行代码会在文件读取完成后执行');

异步操作示例(非阻塞式)

// 异步读取文件(Node.js)
const fs = require('fs');
fs.readFile('file.txt', (err, data) => {
    // 这个回调函数会在文件读取完成后执行
    if (err) throw err;
    console.log(data);
});
console.log('这行代码会立即执行,不必等待文件读取完成');

异步操作的常见场景

  1. 网络请求:

    fetch('<https://api.example.com/data>')
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
    
    console.log('这个日志会立即显示,不必等待请求完成');
  2. 定时操作:

    setTimeout(() => {
      console.log('这条消息将在2秒后显示');
    }, 2000);
    console.log('这条消息会立即显示');
  3. 数据库操作:

    // 伪代码示例
    database.query('SELECT * FROM users', (err, results) => {
      if (err) throw err;
      console.log(results);
    });
    console.log('查询已发起,继续执行其他代码');
  4. 用户交互:

    document.getElementById('button').addEventListener('click', () => {
      console.log('按钮被点击了');
    });
    console.log('已设置点击监听器,继续执行其他代码');

异步编程的实现方式

  1. 回调函数(最基础的方式):

    function asyncOperation(callback) {
      setTimeout(() => {
        callback('操作完成');
      }, 1000);
    }
    
    asyncOperation((message) => {
      console.log(message); // 1秒后输出"操作完成"
    });
  2. Promise(ES6引入):

    function asyncOperation() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve('操作完成');
        }, 1000);
      });
    }
    
    asyncOperation()
      .then(message => console.log(message));
  3. async/await(ES2017引入,语法糖):

    async function doWork() {
      const message = await asyncOperation();
      console.log(message);
    }
    
    doWork();

为什么需要异步操作?

  1. 性能优化:避免阻塞主线程,特别是在单线程环境(如浏览器中的JavaScript)
  2. 用户体验:保持UI响应流畅,避免"卡死"现象
  3. 资源利用:在等待I/O操作(如磁盘读写、网络请求)时,CPU可以处理其他任务
  4. 可扩展性:适合处理高并发场景(如服务器同时处理多个请求)

异步操作的挑战

  1. 代码复杂性:回调嵌套可能导致"回调地狱"
  2. 错误处理:异步错误不像同步错误那样可以通过try-catch直接捕获
  3. 调试困难:执行流程不如同步代码直观
  4. 状态管理:多个异步操作之间的协调可能变得复杂

实际应用示例

多个异步操作并行执行

// 使用Promise.all
const fetchUser = fetch('/user');
const fetchPosts = fetch('/posts');

Promise.all([fetchUser, fetchPosts])
  .then(responses => Promise.all(responses.map(r => r.json())))
  .then(([user, posts]) => {
    console.log('用户数据:', user);
    console.log('帖子数据:', posts);
  });

异步操作串行执行

// 使用async/await
async function getData() {
  const user = await fetch('/user').then(r => r.json());
  const posts = await fetch('/posts').then(r => r.json());

  console.log('用户数据:', user);
  console.log('帖子数据:', posts);
}

异步操作是现代编程中不可或缺的概念,特别是在Web开发、Node.js、移动应用开发等领域。理解异步编程是成为高效开发者的关键一步。

直接调用 db.Delete(&article) 仅会删除 articles 表中的记录,而不会自动清除多对多关联关系(即 article_tags 连接表中的记录)。

在 GORM 中,对于多对多关联,如果需要同时删除关联关系,需要使用 Association Mode 来额外处理,例如:

// 删除文章所有 Tag 关联(删除连接表中的记录)
db.Model(&article).Association("Tags").Clear()

或者,可以配置数据库中的外键约束(例如添加 ON DELETE CASCADE),不过这需要在数据库层面配置,是另一种实现方式。

什么是回调函数?

回调函数是一种编程模式,指的是将一个函数作为参数传递给另一个函数,并在特定条件或事件发生时由后者调用前者。回调函数允许异步操作和事件驱动的编程。

简单来说:回调函数是你定义好但由别人(通常是系统或其他函数)在适当时候调用的函数。

回调函数的主要特点

  1. 函数作为参数:回调函数通常作为参数传递给另一个函数
  2. 延迟执行:回调函数不会立即执行,而是在特定条件满足时执行
  3. 异步处理:常用于处理异步操作的结果
  4. 解耦:将调用者与被调用者分离,提高代码灵活性

回调函数的例子

1. JavaScript中的简单回调

// 定义一个回调函数
function greeting(name) {
    console.log('Hello ' + name);
}

// 定义一个接收回调的函数
function processUserInput(callback) {
    const name = prompt('Please enter your name.');
    callback(name); // 在适当时候调用回调
}

// 使用回调
processUserInput(greeting);

2. Node.js中的文件读取(异步I/O)

const fs = require('fs');

// 回调函数
function fileReadCallback(err, data) {
    if (err) {
        console.error('Error reading file:', err);
        return;
    }
    console.log('File content:', data);
}

// 异步读取文件,完成后调用回调
fs.readFile('example.txt', 'utf8', fileReadCallback);

3. 事件监听(浏览器DOM事件)

// 回调函数
function handleClick(event) {
    console.log('Button clicked!', event);
}

// 注册回调
document.getElementById('myButton').addEventListener('click', handleClick);

4. 排序中的比较回调

const numbers = [3, 1, 4, 1, 5, 9, 2, 6];

// 使用回调函数定义排序规则
numbers.sort(function(a, b) {
    return a - b; // 升序排序
});

console.log(numbers); // [1, 1, 2, 3, 4, 5, 6, 9]

回调函数的优缺点

优点:

  • 实现异步编程
  • 提高代码复用性
  • 解耦调用者和被调用者
  • 适用于事件驱动编程

缺点:

  • 回调地狱(Callback Hell):多层嵌套回调使代码难以阅读和维护
  • 错误处理复杂:需要在每个回调中单独处理错误
  • 控制流不直观

回调地狱示例

// 多层嵌套的回调,难以阅读和维护
doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
        doThirdThing(newResult, function(finalResult) {
            console.log('Got the final result:', finalResult);
        }, failureCallback);
    }, failureCallback);
}, failureCallback);

现代JavaScript通常使用Promise或async/await来解决回调地狱问题。

回调函数是编程中非常重要的概念,特别是在JavaScript和Node.js中广泛使用。理解回调函数是掌握异步编程的基础。

缓存问题:

  • Git 会缓存跟踪状态,即使添加了忽略规则,已跟踪的文件仍会被继续跟踪
  • 解决方案:

    git rm -r --cached /backend/tmp/
    git add .
    git commit -m "停止跟踪 tmp 目录"

安装

go install github.com/gin-gonic/gin@latest

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

引入

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

开始

生成实例:

// 使用默认中间件(logger 和 recovery 中间件)创建 gin 路由
r := gin.Default()

运行

r.Run()
// 默认在 8080 端口启动服务,除非定义了一个 PORT 的环境变量。
// r.Run(":8888") 传入参数切换端口 

路由

r.GET("/someGet", getting)
r.POST("/somePost", posting)
r.PUT("/somePut", putting)
r.DELETE("/someDelete", deleting)
r.PATCH("/somePatch", patching)
r.HEAD("/someHead", head)
r.OPTIONS("/someOptions", options)

注:

对于每一种路由都是传入两个参数,一个是路由的路径,第二个是处理请求的函数

对于请求函数例如:

func(c *gin.Context) {

    // 其他处理代码
    ...
    // 其他处理代码
    
    // 响应
    c.String(http.StatusOK, "hello world")
}

需要传入一个参数 c 为gin的上下文类型

gin中存在多种不同的响应类型

  1. 返回JSON数据
func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回HTML模板
func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "title": "Main website",
    })
}

// gin.H{} 传递给模板,模板中使用{{.title}}使用,其他的更多用法比如循环,分支,之类可以查找go官方文档
// https://pkg.go.dev/text/template
// https://pkg.go.dev/html/template

需要先设置模板目录:

router := gin.Default()
router.LoadHTMLGlob("templates/*")
  1. 返回XML数据
func(c *gin.Context) {
    c.XML(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回YAML数据
func(c *gin.Context) {
    c.YAML(http.StatusOK, gin.H{
        "message": "hello world",
        "status":  "success",
    })
}
  1. 返回ProtoBuf数据
func(c *gin.Context) {
    data := &protoexample.Test{
        Label: proto.String("hello"),
        Reps:  []int64{1, 2, 3},
    }
    c.ProtoBuf(http.StatusOK, data)
}
  1. 返回文件
func(c *gin.Context) {
    c.File("/path/to/file")
}
  1. 返回文件流
func(c *gin.Context) {
    c.FileFromFS("/path/to/file", http.Dir("/var/www"))
}
  1. 返回数据流
func(c *gin.Context) {
    reader := strings.NewReader("some data")
    c.DataFromReader(http.StatusOK, int64(reader.Len()), "text/plain", reader, nil)
}
  1. 重定向
func(c *gin.Context) {
    c.Redirect(http.StatusMovedPermanently, "<https://www.google.com/>")
}

func(c *gin.Context) {
    c.Request.URL.Path = "/"
    r.HandleContext(c)
}
  1. 返回纯文本
func(c *gin.Context) {
    c.String(http.StatusOK, "hello world")
}

关于Gin框架中的gin.H

gin.H是Gin框架中一个非常实用的快捷方式,它实际上是一个类型别名(type alias),定义如下:

type H map[string]interface{}

详细解释

  1. 本质:gin.H就是一个map[string]interface{}的别名

    • string是键的类型
    • interface{}是值的类型,表示可以接受任何类型的值
  2. 用途:

    • 主要用于构建JSON/XML/YAML等格式的响应数据
    • 提供了一种简洁的方式来创建动态数据结构
  3. 示例:
// 创建一个简单的JSON响应
data := gin.H{
    "name": "John",
    "age":  30,
    "tags": []string{"golang", "gin"},
}
c.JSON(http.StatusOK, data)
  1. 优点:

    • 比直接使用map[string]interface{}更简洁
    • 提高了代码可读性
    • 是Gin框架中的惯用写法
  2. 与其他方式的对比:
// 使用gin.H
gin.H{"key": "value"}

// 等同于
map[string]interface{}{"key": "value"}

// 也等同于
struct {
    Key string `json:"key"`
}{Key: "value"}
  1. 嵌套使用:
data := gin.H{
    "user": gin.H{
        "name": "Alice",
        "age":  25,
    },
    "status": "active",
}
  1. 注意事项:

    • 虽然方便,但对于复杂数据结构,有时使用自定义结构体可能更合适
    • 在性能敏感的场景,直接使用结构体可能比gin.H更高效

gin.H是Gin框架中一个简单但强大的工具,它简化了动态数据的构建过程,特别是在构建API响应时非常有用。

在Gin中使用自定义结构体的示例

当然可以。在Gin框架中,使用自定义结构体作为响应通常比gin.H更规范,特别是在处理复杂数据结构时。以下是几个示例:

  1. 基本结构体示例
// 定义自定义结构体
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

func getUser(c *gin.Context) {
    user := User{
        ID:       1,
        Username: "john_doe",
        Email:    "[email protected]",
    }
    c.JSON(http.StatusOK, user)
}
  1. 嵌套结构体示例
type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

type UserProfile struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
}

func getProfile(c *gin.Context) {
    profile := UserProfile{
        Name: "Alice",
        Age:  28,
        Address: Address{
            City:    "Shanghai",
            Country: "China",
        },
    }
    c.JSON(http.StatusOK, profile)
}
  1. 带切片的结构体
type Post struct {
    ID      int      `json:"id"`
    Title   string   `json:"title"`
    Tags    []string `json:"tags"`
}

func getPosts(c *gin.Context) {
    posts := []Post{
        {
            ID:    1,
            Title: "Gin框架入门",
            Tags:  []string{"golang", "web"},
        },
        {
            ID:    2,
            Title: "Go语言并发编程",
            Tags:  []string{"golang", "concurrency"},
        },
    }
    c.JSON(http.StatusOK, posts)
}
  1. 作为请求体和响应体
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type LoginResponse struct {
    Token string `json:"token"`
    Exp   int64  `json:"exp"`
}

func login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 验证逻辑...

    resp := LoginResponse{
        Token: "generated_token_here",
        Exp:   time.Now().Add(24 * time.Hour).Unix(),
    }
    c.JSON(http.StatusOK, resp)
}
  1. 带omitempty标签的示例
type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Price       float64 `json:"price"`
    Description string  `json:"description,omitempty"` // 为空时不显示
    Stock       int     `json:"-"`                    // 永远不序列化
}

func getProduct(c *gin.Context) {
    product := Product{
        ID:    101,
        Name:  "Laptop",
        Price: 999.99,
        // Description和Stock未赋值
    }
    c.JSON(http.StatusOK, product)
    // 输出: {"id":101,"name":"Laptop","price":999.99}
}

Gin框架中的参数传递方式

一、路由传入的参数类型

  1. 路径参数 (Path Parameters)
router.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // 获取路径参数
})
  1. 查询参数 (Query Parameters)
// 请求: /search?q=gin&page=1
q := c.Query("q")         // "gin"
page := c.DefaultQuery("page", "1") // 带默认值
  1. 表单参数 (Form Data)
// Content-Type: application/x-www-form-urlencoded
name := c.PostForm("name")
age := c.DefaultPostForm("age", "18") // 带默认值
  1. 多部分表单 (Multipart/Form-Data)

用于文件上传:

file, _ := c.FormFile("file")

// 处理文件上传
if file, err := c.FormFile("avatar"); err == nil {
    // 保存文件逻辑
    user.Avatar = "/uploads/" + file.Filename
}
  1. Header 参数
token := c.GetHeader("Authorization")
  1. Cookie 参数
value, err := c.Cookie("cookie_name")
  1. 原始Body (Raw Body)
body, _ := ioutil.ReadAll(c.Request.Body)

二、POST请求可传递的参数类型

POST请求可以通过以下Content-Type传递不同类型的参数:

  1. application/x-www-form-urlencoded
  • 传统的表单提交格式
  • 通过c.PostForm()获取
  • 示例:
// 客户端发送: name=John&age=30
name := c.PostForm("name")
  1. multipart/form-data
  • 用于文件上传和表单混合数据
  • 通过c.FormFile()获取文件
  • 通过c.PostForm()获取普通字段
  • 示例:
file, _ := c.FormFile("avatar")
name := c.PostForm("username")
  1. application/json
  • 最常用的API数据格式
  • 通过c.ShouldBindJSON()绑定到结构体
  • 示例:
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

var user User
if err := c.ShouldBindJSON(&user); err != nil {
    // 错误处理
}

//例如:
// 绑定JSON或Form数据
//var user User
//if err := c.ShouldBind(&user); err != nil {
//    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
//    return
//}
  1. text/xml / application/xml
  • XML格式数据
  • 通过c.ShouldBindXML()绑定
  • 示例:
if err := c.ShouldBindXML(&obj); err != nil {
    // 错误处理
}
  1. application/protobuf
  • Protocol Buffers格式
  • 高性能二进制格式
  • 示例:
var message pb.Message
if err := proto.Unmarshal(c.Request.Body, &message); err != nil {
    // 错误处理
}
  1. 其他自定义格式

可以通过c.GetRawData()获取原始数据后自行解析:

data, _ := c.GetRawData()
// 自定义解析逻辑

三、参数绑定最佳实践

  1. 对于简单参数:直接使用Query(), PostForm()等方法
  2. 对于复杂结构:使用ShouldBind系列方法绑定到结构体
  3. 文件上传:使用FormFile()
  4. API开发:优先使用JSON格式
  5. 性能敏感场景:考虑使用protobuf

四、完整示例

type User struct {
    ID       int    `json:"id" form:"id"`
    Username string `json:"username" form:"username"`
    Avatar   string `json:"avatar"` // 文件上传单独处理
}

func updateUser(c *gin.Context) {
    // 路径参数
    id := c.Param("id")

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

    // 处理文件上传
    if file, err := c.FormFile("avatar"); err == nil {
        // 保存文件逻辑
        user.Avatar = "/uploads/" + file.Filename
    }

    // 处理其他逻辑...
}

分组路由(Grouping Routes)

如果有一组路由,前缀都是/api/v1开头,是否每个路由都需要加上/api/v1这个前缀呢?答案是不需要,分组路由可以解决这个问题。利用分组路由还可以更好地实现权限控制,例如将需要登录鉴权的路由放到同一分组中去,简化权限控制。

// group routes 分组路由
defaultHandler := func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "path": c.FullPath(),
    })
}
// group: v1
v1 := r.Group("/v1")
{
    v1.GET("/posts", defaultHandler)
    v1.GET("/series", defaultHandler)
}
// group: v2
v2 := r.Group("/v2")
{
    v2.GET("/posts", defaultHandler)
    v2.GET("/series", defaultHandler)
}

中间件使用

示例:

// 作用于全局
r.Use(gin.Logger())
r.Use(gin.Recovery())

// 作用于单个路由
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

// 作用于某个组
authorized := r.Group("/")
authorized.Use(AuthRequired())
{
    authorized.POST("/login", loginEndpoint)
    authorized.POST("/submit", submitEndpoint)
}

参考文档:

https://gin-gonic.com/zh-cn/docs/

https://geektutu.com/post/quick-go-gin.html