首页 后端开发 Golang 使用 ginvalidator 简化 Go 中的 Gin 输入验证

使用 ginvalidator 简化 Go 中的 Gin 输入验证

Dec 01, 2024 am 11:52 AM

Simplify Gin Input Validation in Go with ginvalidator

概述

ginvalidator 是一组 Gin 中间件,它包装了我的另一个开源包 validatorgo 提供的广泛的验证器和消毒器集合。它还使用流行的开源包 gjson 进行 JSON 字段语法,提供从 JSON 对象中高效查询和提取数据的功能。

它允许您以多种方式组合它们,以便您可以验证和清理您的 Gin 请求,并提供工具来确定请求是否有效,以及根据您的验证器匹配哪些数据。

它基于流行的js/express库express-validator

支持

此版本的 ginvalidator 要求您的应用程序在 Go 1.16 上运行。
它还经验证可与 Gin 1.x.x.

配合使用

基本原理

为什么不使用?

  • 手写验证器: 您可以手动编写自己的验证逻辑,但这很快就会变得重复且混乱。每次需要新的验证时,您都在一遍又一遍地编写相同类型的代码。很容易出错,而且维护起来很痛苦。
  • Gin 内置的模型绑定和验证: Gin 内置了验证,但它并不适合所有人。结构标签具有限制性,并使您的代码更难以阅读,尤其是当您需要复杂的规则时。另外,验证与模型联系得太紧密,这不利于灵活性。
  • 其他库(如 Galidator): 还有其他库,但它们通常感觉对其所做的事情来说过于复杂。它们需要比您预期更多的设置和工作,特别是当您只想要一个简单、直接的验证解决方案时。

安装

确保您的计算机上安装了 Go。

第 1 步:创建一个新的 Go 模块

  1. 使用您选择的名称创建一个空文件夹。
  2. 打开终端,导航 (cd) 到该文件夹​​,并初始化一个新的 Go 模块:
go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

第2步:安装所需的软件包

使用 go get 安装必要的软件包。

  1. 安装杜松子酒:
go get -u github.com/gin-gonic/gin
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
  1. 安装ginvalidator:
go get -u github.com/bube054/ginvalidator
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

入门

学习东西的最好方法之一就是通过实例!因此,让我们卷起袖子,开始编写代码吧。

设置

首先需要的是运行一个 Gin 服务器。让我们实现一个向某人打招呼的功能;为此,创建一个 main.go 然后添加以下代码:

package main

import (
    "net/http"

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

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

    r.GET("/hello", func(ctx *gin.Context) {
        person := ctx.Query("person")
        ctx.String(http.StatusOK, "Hello, %s!", person)
    })

    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制

现在通过在终端上执行 go run main.go 来运行此文件。

go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

HTTP 服务器应该正在运行,您可以打开 http://localhost:8080/hello?person=John 向 John 致敬!

提示:
您可以将 Air 与 Go 和 Gin 结合使用来实现实时重新加载。每当文件更改时,这些都会自动重新启动服务器,因此您不必自己执行此操作!

添加验证器

所以服务器正在工作,但存在问题。最值得注意的是,当某人的名字未设置时,您不想向某人打招呼。
例如,访问 http://localhost:8080/hello 将打印“Hello,”。

这就是 ginvalidator 派上用场的地方。它提供了用于验证您的请求的验证器、消毒器和修饰符。
让我们添加一个验证器和一个修饰符来检查人员查询字符串不能为空,验证器名为 Empty ,修饰符名为 Not:

go get -u github.com/gin-gonic/gin
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

注意:

为简洁起见,代码示例中使用 gv 作为 ginvalidator 的别名。

现在,重新启动服务器,然后再次访问 http://localhost:8080/hello。嗯,它仍然打印“Hello,!”...为什么?

处理验证错误

ginvalidator验证链不会自动向用户报告验证错误。
原因很简单:当您添加更多验证器或更多字段时,您希望如何收集错误?您想要一份所有错误的列表,每个字段只有一个,整体只有一个......?

所以下一个明显的步骤是再次更改上面的代码,这次使用 ValidationResult 函数验证验证结果:

go get -u github.com/bube054/ginvalidator
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

现在,如果您再次访问 http://localhost:8080/hello,您将看到以下 JSON 内容,为了清晰起见,我们进行了格式化:

package main

import (
    "net/http"

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

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

    r.GET("/hello", func(ctx *gin.Context) {
        person := ctx.Query("person")
        ctx.String(http.StatusOK, "Hello, %s!", person)
    })

    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制

现在,这告诉我们的是

  • 此请求中只有一个错误;
  • 这个字段称为 person;
  • 它位于查询字符串中(位置:“queries”);
  • 给出的错误消息是“无效值”。

这是一个更好的场景,但仍然可以改进。我们继续吧。

创建更好的错误消息

所有请求位置验证器都接受可选的第二个参数,它是用于格式化错误消息的函数。如果提供 nil,将使用默认的通用错误消息,如上面的示例所示。

go run main.go
登录后复制
登录后复制

现在,如果您再次访问 http://localhost:8080/hello,您将看到以下 JSON 内容,并带有新的错误消息:

package main

import (
    "net/http"

    gv "github.com/bube054/ginvalidator"
    "github.com/gin-gonic/gin"
)

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

    r.GET("/hello", gv.NewQuery("person", nil).
        Chain().
        Not().
        Empty(nil).
        Validate(), func(ctx *gin.Context) {
            person := ctx.Query("person")
            ctx.String(http.StatusOK, "Hello, %s!", person)
        })

    r.Run()
}
登录后复制
登录后复制

访问经过验证/清理的数据

您可以使用 GetMatchedData,它会自动收集 ginvalidator 已验证和/或清理的所有数据。然后可以使用 MatchedData 的 Get 方法访问此数据:

go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

打开 http://localhost:8080/hello?person=John 向 John 致敬!

可用位置有 BodyLocation、CookieLocation、QueryLocation、ParamLocation 和 HeaderLocation。
每个位置都包含一个 String 方法,该方法返回存储经过验证/清理的数据的位置。

净化输入

虽然用户不能再发送空人名,但它仍然可以将 HTML 注入您的页面!这称为跨站脚本漏洞 (XSS)。
让我们看看它是如何工作的。转到 http://localhost:8080/hello?person=John,您应该看到“Hello, John!”。
虽然此示例很好,但攻击者可以将人员查询字符串更改为 <script> 。加载自己的 JavaScript 的标签可能有害。<br> 在这种情况下,缓解 ginvalidator 问题的一种方法是使用消毒剂,特别是 Escape,它将特殊的 HTML 字符与其他可以表示为文本的字符进行转换。<br> </script>

go get -u github.com/gin-gonic/gin
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

现在,如果您重新启动服务器并刷新页面,您将看到“Hello, John!”。我们的示例页面不再容易受到 XSS 攻击!

⚠️ 注意:

ginvalidator 在清理期间不会修改 http.Request 值。要访问清理后的数据,请始终使用 GetMatchedData 函数。

验证链

验证链是 ginvalidator 中的主要概念之一,因此了解它很有用,以便您可以有效地使用它。

但是不用担心:如果您已经阅读了入门指南,那么您已经在不知不觉中使用了验证链!

什么是验证链?

验证链是使用以下函数创建的,每个函数都针对 HTTP 请求中的特定位置:

  • NewBody:验证来自 http.Request 正文的数据。它的位置是 BodyLocation。
  • NewCookie:验证来自 http.Request cookie 的数据。它的位置是 CookieLocation。
  • NewHeader:验证来自 http.Request 标头的数据。它的位置是 HeaderLocation。
  • NewParam:验证 Gin 路由参数中的数据。它的位置是 ParamLocation。
  • NewQuery:验证来自 http.Request 查询参数的数据。它的位置是 QueryLocation。

它们之所以有这个名字,是因为它们用验证(或清理)来包装字段的值,并且它的每个方法都返回自身。
这种模式通常称为方法链,因此称为验证链。

验证链不仅具有许多用于定义验证、清理和修改的有用方法,而且还具有返回 Gin 中间件处理函数的 Validate 方法。

这是验证链通常如何使用以及如何阅读的示例:

go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

特征

验证链具有三种方法:验证器、消毒器和修饰器。

验证器确定请求字段的值是否有效。这意味着检查该字段是否采用您期望的格式。例如,如果您正在构建注册表单,您的要求可能是用户名必须是电子邮件地址,并且密码的长度必须至少为 8 个字符。

如果该值无效,则会使用一些错误消息记录该字段的错误。然后可以稍后在 Gin 路由处理程序中检索此验证错误并将其返回给用户。

他们是:

  • 自定义验证器
  • 包含
  • 等于
  • 阿坝路由
  • 之后
  • 阿尔法
  • 字母数字
  • ASCII
  • Base32
  • Base58
  • Base64
  • 之前
  • 比克
  • 布尔值
  • BTC地址
  • 字节长度
  • 信用卡
  • 货币
  • DataURI
  • 日期
  • 十进制
  • 可整除
  • EAN
  • 电子邮件
  • 以太坊地址
  • 漂浮
  • FQDN
  • 货运集装箱ID
  • 全宽
  • 半宽
  • 哈希
  • 十六进制
  • 十六进制颜色
  • HSL
  • 国际银行账号
  • 身份证
  • IMEI
  • 国际
  • IP
  • IP范围
  • ISIN
  • ISO4217
  • ISO6346
  • ISO6391
  • ISO8601
  • ISO31661Alpha2
  • ISO31661Alpha3
  • ISO31661 数字
  • ISRC
  • ISSN
  • JSON
  • 拉长
  • 车牌
  • 语言环境
  • 小写
  • LuhnNumber
  • Mac地址
  • MagnetURI
  • MailtoURI
  • MD5
  • 哑剧类型
  • 手机
  • MongoID
  • 多字节
  • 数字
  • 八进制
  • 护照号码
  • 港口
  • 邮政编码
  • RFC3339
  • RGB颜色
  • SemVer
  • 蛞蝓
  • 强密码
  • 税号
  • 代理对
  • 时间
  • ULID
  • 大写
  • 网址
  • UUID
  • 可变宽度
  • 增值税
  • 已列入白名单
  • 比赛

消毒剂会改变字段值。它们对于消除值中的噪音很有用,甚至可能提供一些针对威胁的基本防线。

Sanitizers 将更新后的字段值保留回 Gin 上下文中,以便其他 ginvalidator 函数、您自己的路由处理程序代码,甚至其他中间件都可以使用它。

他们是:

  • 定制消毒剂
  • 黑名单
  • 逃脱
  • LTrim
  • 标准化电子邮件
  • RTrim
  • 脱衣低
  • 转布尔值
  • 迄今为止
  • 漂浮
  • ToInt
  • 修剪
  • 逃亡
  • 白名单

修饰符定义验证链运行时的行为方式。

他们是:

  • 保释
  • 如果
  • 不是
  • 跳过
  • 可选

注意:

这些方法在 pkg.go.dev ginvalidator 文档中使用 GoDoc 进行了完整记录。如果有任何细节不清楚,您可能还需要参考 validatorgo 包中的相关函数以获取更多上下文,我将在下面进行解释。

标准验证器/消毒器

验证链公开的所有功能实际上都来自 validatorgo,这是我的其他开源 go 包之一,专门从事字符串验证/清理。请检查一下,加星标并分享???,谢谢。

这包括所有 validatorgo 验证器和清理程序,从常用的 IsEmail、IsLength 和 Trim 到更小众的 IsISBN、IsMultibyte 和 StripLow!

这些在 ginvalidator 中被称为标准验证器和标准消毒器。但没有来自 validatorgo 的 Is 前缀。

链接顺序

在验证链上调用方法的顺序通常很重要。
它们几乎总是按照指定的顺序运行,因此您只需阅读验证链的定义(从第一个链接方法到最后一个方法)就可以知道验证链将做什么。

以下面的代码片段为例:

go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

在这种情况下,如果用户传递一个仅由空格组成的“search_query”值,它不会为空,因此验证通过。但由于 .Trim() 消毒剂存在,空格将被删除,并且该字段将变为空,因此您实际上最终会得到误报。

现在,将其与以下代码片段进行比较:

go get -u github.com/gin-gonic/gin
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

这条链将更明智地删除空格,然后验证值是否不为空。

此规则的一个例外是 .Optional():它可以放置在链中的任何一点,并将该链标记为可选。

重用验证链

如果您希望重用同一个链,最好从函数中返回它们:

go get -u github.com/bube054/ginvalidator
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

领域选择

在 ginvalidator 中,字段是任何经过验证或清理的值,并且它是字符串。

几乎每个函数或值都以某种方式由 ginvalidator 引用字段返回。因此,了解字段路径语法对于选择验证字段以及访问验证错误或验证数据时非常重要。

句法

  • 正文字段仅对以下内容类型有效:

    • application/json:这使用 GJSON 路径语法来提取值。详细信息请参阅链接文档。
    • 示例
    go mod init example.com/learning
    
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    使用路径 user.name,提取的值为“John”。

    • application/x-www-form-urlencoded:通常用于 HTML 表单提交。字段在正文中作为键值对提交。
    • 示例
    go get -u github.com/gin-gonic/gin
    
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    身体:

    go get -u github.com/bube054/ginvalidator
    
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    字段“name”的值为“John”,“email”的值为“john.doe@example.com”。

    • multipart/form-data:常用于文件上传或随文件提交表单数据时。
    • 示例
    package main
    
    import (
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/hello", func(ctx *gin.Context) {
            person := ctx.Query("person")
            ctx.String(http.StatusOK, "Hello, %s!", person)
        })
    
        r.Run() // listen and serve on 0.0.0.0:8080
    }
    
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    身体:

    go run main.go
    
    登录后复制
    登录后复制

    字段“name”的值为“John”,“file”为上传的文件。

  • 查询字段对应URL搜索参数,其值自动为Gin未转义的url。

    示例:

    • 字段:“姓名”,值:“约翰”
    package main
    
    import (
        "net/http"
    
        gv "github.com/bube054/ginvalidator"
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/hello", gv.NewQuery("person", nil).
            Chain().
            Not().
            Empty(nil).
            Validate(), func(ctx *gin.Context) {
                person := ctx.Query("person")
                ctx.String(http.StatusOK, "Hello, %s!", person)
            })
    
        r.Run()
    }
    
    登录后复制
    登录后复制
    • 字段:“full_name”,值:“John Doe”
    package main
    
    import (
        "net/http"
    
        gv "github.com/bube054/ginvalidator"
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/hello",
            gv.NewQuery("person", nil).
                Chain().
                Not().
                Empty(nil).
                Validate(),
            func(ctx *gin.Context) {
                result, err := gv.ValidationResult(ctx)
                if err != nil {
                    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                        "message": "The server encountered an unexpected error.",
                    })
                    return
                }
    
                if len(result) != 0 {
                    ctx.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
                        "errors": result,
                    })
                    return
                }
    
                person := ctx.Query("person")
                ctx.String(http.StatusOK, "Hello, %s!", person)
            })
    
        r.Run()
    }
    
    登录后复制
  • Param 字段 表示 URL 路径参数,其值会被 ginvalidator 自动转义。

    示例:

    • 字段:“id”,值:“123”
    {
      "errors": [
        {
          "location": "queries",
          "message": "Invalid value",
          "field": "person",
          "value": ""
        }
      ]
    }
    
    登录后复制
  • 标头字段是HTTP请求标头,它们的值不是未转义的。如果您提供非规范标头密钥,将会出现日志警告。

    示例:

    • 字段:“用户代理”,值:“Mozilla/5.0”
    package main
    
    import (
        "net/http"
    
        gv "github.com/bube054/ginvalidator"
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/hello",
            gv.NewQuery("person",
                func(initialValue, sanitizedValue, validatorName string) string {
                    return "Please enter your name."
                },
            ).Chain().
                Not().
                Empty(nil).
                Validate(),
            func(ctx *gin.Context) {
                result, err := gv.ValidationResult(ctx)
                if err != nil {
                    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                        "message": "The server encountered an unexpected error.",
                    })
                    return
                }
    
                if len(result) != 0 {
                    ctx.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
                        "errors": result,
                    })
                    return
                }
    
                person := ctx.Query("person")
                ctx.String(http.StatusOK, "Hello, %s!", person)
            })
    
        r.Run()
    }
    
    登录后复制
  • Cookie 字段 是 HTTP cookie,其值自动为 Gin 未转义的 url。

    示例:

    • 字段:“session_id”,值:“abc 123”
    {
      "errors": [
        {
          "location": "queries",
          "message": "Please enter your name.",
          "field": "person",
          "value": ""
        }
      ]
    }
    
    登录后复制

自定义快速验证器

如果您正在构建的服务器不是一个非常简单的服务器,那么迟早您将需要验证器、清理器和错误消息,而不仅仅是 ginvalidator 中内置的内容。

自定义验证器和消毒器

ginvalidator 无法满足您并且您可能会遇到的一个经典需求是在用户注册时验证电子邮件地址是否正在使用。

可以通过实现自定义验证器在 ginvalidator 中执行此操作。

CustomValidator 是验证链上可用的方法,它接收特殊函数 CustomValidatorFunc,并且必须返回一个布尔值来确定字段是否有效。

CustomSanitizer 也是验证链上可用的方法,它接收特殊函数 CustomSanitizerFunc,并且必须返回新的清理值。

实现自定义验证器

CustomValidator 可以通过使用 goroutine 和sync.WaitGroup 来异步处理并发操作。在验证器中,您可以为每个异步任务启动 goroutine,并将每个任务添加到 WaitGroup。所有任务完成后,验证器应返回一个布尔值。

例如,为了检查电子邮件是否未被使用:

go mod init example.com/learning
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

或者您也可以验证密码是否与重复匹配:

go get -u github.com/gin-gonic/gin
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

⚠️ 注意:
如果请求正文将被多次访问(无论是在同一验证链中、在同一请求上下文的另一个验证链中还是在后续处理程序中),请确保在每次读取后重置请求正文。如果不这样做,再次读取正文时可能会导致错误或数据丢失。

实施自定义消毒剂

CustomSanitizer 没有太多规则。无论它们返回什么值,都是该字段将获取的新值。
自定义清理程序还可以通过使用 goroutine 和sync.WaitGroup 来异步处理并发操作。

go get -u github.com/bube054/ginvalidator
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

错误信息

每当字段值无效时,都会为其记录一条错误消息。
默认错误消息是“无效值”,它根本无法描述错误的内容,因此您可能需要对其进行自定义。您可以通过
进行定制

package main

import (
    "net/http"

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

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

    r.GET("/hello", func(ctx *gin.Context) {
        person := ctx.Query("person")
        ctx.String(http.StatusOK, "Hello, %s!", person)
    })

    r.Run() // listen and serve on 0.0.0.0:8080
}
登录后复制
登录后复制
登录后复制
登录后复制
  • 初始值是从请求中提取的原始值(在任何清理之前)。
  • sanitizedValue 是经过清理后的值(如果适用)。
  • validatorName 是失败的验证器的名称,有助于识别未通过的验证规则。

有关验证器名称的完整列表,请参阅 ginvalidator 常量。

维护者

  • bube054 - Attah Gbubemi David(作者)

以上是使用 ginvalidator 简化 Go 中的 Gin 输入验证的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Go语言包导入:带下划线和不带下划线的区别是什么? Go语言包导入:带下划线和不带下划线的区别是什么? Mar 03, 2025 pm 05:17 PM

Go语言包导入:带下划线和不带下划线的区别是什么?

Beego框架中NewFlash()函数如何实现页面间短暂信息传递? Beego框架中NewFlash()函数如何实现页面间短暂信息传递? Mar 03, 2025 pm 05:22 PM

Beego框架中NewFlash()函数如何实现页面间短暂信息传递?

Go语言中如何将MySQL查询结果List转换为自定义结构体切片? Go语言中如何将MySQL查询结果List转换为自定义结构体切片? Mar 03, 2025 pm 05:18 PM

Go语言中如何将MySQL查询结果List转换为自定义结构体切片?

如何定义GO中仿制药的自定义类型约束? 如何定义GO中仿制药的自定义类型约束? Mar 10, 2025 pm 03:20 PM

如何定义GO中仿制药的自定义类型约束?

如何编写模拟对象和存根以进行测试? 如何编写模拟对象和存根以进行测试? Mar 10, 2025 pm 05:38 PM

如何编写模拟对象和存根以进行测试?

Go语言如何便捷地写入文件? Go语言如何便捷地写入文件? Mar 03, 2025 pm 05:15 PM

Go语言如何便捷地写入文件?

您如何在GO中编写单元测试? 您如何在GO中编写单元测试? Mar 21, 2025 pm 06:34 PM

您如何在GO中编写单元测试?

如何使用跟踪工具了解GO应用程序的执行流? 如何使用跟踪工具了解GO应用程序的执行流? Mar 10, 2025 pm 05:36 PM

如何使用跟踪工具了解GO应用程序的执行流?

See all articles