• 简体中文
  • 模糊测试入门指南

    本教程介绍Go语言中模糊测试的基础知识。模糊测试通过向测试程序输入随机数据,试图发现漏洞或导致崩溃的输入。模糊测试可发现的漏洞类型包括SQL注入、缓冲区溢出、拒绝服务攻击和跨站脚本攻击等。

    在本教程中,您将为一个简单函数编写模糊测试,运行 go 命令,并调试修复代码中的问题。

    若需了解本教程中的术语,请参阅Go模糊测试词汇表

    您将依次完成以下步骤:

    1. 创建代码文件夹
    2. 添加待测代码
    3. 添加单元测试
    4. 添加模糊测试
    5. 修复两个漏洞
    6. 探索更多资源

    注意:Go模糊测试目前仅支持部分内置类型,具体列表详见 Go模糊测试文档,未来将逐步增加更多内置类型的支持。

    先决条件

    • Go 1.18 或更高版本的安装。 有关安装说明,请参阅 安装 Go
    • 一个用于编辑代码的工具。 任何你拥有的文本编辑器都可以正常工作。
    • 一个命令终端。 Go 在 Linux 和 Mac 上的任何终端上都能很好地工作,在 Windows 上的 PowerShell 或 cmd 上也是如此。
    • 支持模糊测试的环境。 目前,Go 模糊测试与覆盖检测仅在 AMD64 和 ARM64 架构上可用。

    为代码创建一个文件夹

    首先,为要编写的代码创建一个文件夹。

    1. 打开命令提示符并切换到你的主目录。

      在 Linux 或 Mac 上:

      $ cd

      在 Windows 上:

      C:\> cd %HOMEPATH%

      本教程的其余部分将显示 $ 作为提示符。你使用的命令在 Windows 上也能工作。

    2. 从命令提示符,创建一个名为 fuzz 的代码目录。

      $ mkdir fuzz
      $ cd fuzz
    3. 创建一个模块来保存你的代码。

      运行 go mod init 命令,给它你新代码的模块路径。

      $ go mod init example/fuzz
      go: creating new go.mod: module example/fuzz

      注意: 对于生产代码,你将指定一个更具体符合你自己需求的模块路径。有关更多信息,请参阅 管理依赖项

    接下来,你将添加一些简单的代码来反转字符串,稍后我们将对其进行模糊测试。

    添加测试代码

    在此步骤中,你将添加一个函数来反转字符串。

    编写代码

    1. 使用你的文本编辑器,在 fuzz 目录中创建一个名为 main.go 的文件。

    2. 将以下包声明粘贴到 main.go 的顶部。

      package main

      一个独立的程序(与库相对)始终在 main 包中。

    3. 在包声明下方,粘贴以下函数声明。

      func Reverse(s string) string {
          b := []byte(s)
          for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
              b[i], b[j] = b[j], b[i]
          }
          return string(b)
      }

      此函数将接受一个 string,逐个字节地循环遍历它,并在最后返回反转后的字符串。

      注意: 此代码基于 golang.org/x/example 中的 stringutil.Reverse 函数。

    4. main.go 的顶部,包声明下方,粘贴以下 main 函数以初始化一个字符串,反转它,打印输出,并重复。

      func main() {
          input := "The quick brown fox jumped over the lazy dog"
          rev := Reverse(input)
          doubleRev := Reverse(rev)
          fmt.Printf("original: %q\n", input)
          fmt.Printf("reversed: %q\n", rev)
          fmt.Printf("reversed again: %q\n", doubleRev)
      }

      此函数将运行一些 Reverse 操作,然后将输出打印到命令行。这对于查看代码的实际运行情况和潜在的调试很有帮助。

    5. main 函数使用了 fmt 包,因此你需要导入它。

      代码的前几行应如下所示:

      package main
      
      import "fmt"

    运行代码

    在包含 main.go 的目录的命令行中,运行代码。

    $ go run .
    original: "The quick brown fox jumped over the lazy dog"
    reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
    reversed again: "The quick brown fox jumped over the lazy dog"

    你可以看到原始字符串,反转后的结果,然后再反转一次的结果,这与原始字符串相同。

    现在代码正在运行,是时候测试它了。

    添加单元测试

    在此步骤中,你将为 Reverse 函数编写一个基本的单元测试。

    编写代码

    1. 使用你的文本编辑器,在 fuzz 目录中创建一个名为 reverse_test.go 的文件。

    2. 将以下代码粘贴到 reverse_test.go

      package main
      
      import (
          "testing"
      )
      
      func TestReverse(t *testing.T) {
          testcases := []struct {
              in, want string
          }{
              {"Hello, world", "dlrow ,olleH"},
              {" ", " "},
              {"!12345", "54321!"},
          }
          for _, tc := range testcases {
              rev := Reverse(tc.in)
              if rev != tc.want {
                      t.Errorf("Reverse: %q, want %q", rev, tc.want)
              }
          }
      }

      这个简单的测试将断言列出的输入字符串将被正确反转。

    运行代码

    使用 go test 运行单元测试

    $ go test
    PASS
    ok      example/fuzz  0.013s

    接下来,你将把单元测试改为模糊测试。

    添加模糊测试

    单元测试有一些局限性,即每个输入都必须由开发者添加到测试中。模糊测试的一个好处是它能为你的代码生成输入,并可能识别出你设想的测试用例未触及的边缘情况。

    在本节中,你将把单元测试转换为模糊测试,这样你就可以用较少的工作生成更多输入!

    请注意,你可以在同一个 *_test.go 文件中保留单元测试、基准测试和模糊测试,但在这个例子中,你将把单元测试转换为模糊测试。

    编写代码

    在你的文本编辑器中,将 reverse_test.go 中的单元测试替换为以下模糊测试。

    func FuzzReverse(f *testing.F) {
        testcases := []string{"Hello, world", " ", "!12345"}
        for _, tc := range testcases {
            f.Add(tc)  // 使用 f.Add 提供种子语料库
        }
        f.Fuzz(func(t *testing.T, orig string) {
            rev := Reverse(orig)
            doubleRev := Reverse(rev)
            if orig != doubleRev {
                t.Errorf("Before: %q, after: %q", orig, doubleRev)
            }
            if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
            }
        })
    }

    模糊测试也有一些局限性。在你的单元测试中,你可以预测 Reverse 函数的预期输出,并验证实际输出是否符合这些期望。

    例如,在测试用例 Reverse("Hello, world") 中,单元测试指定返回值为 "dlrow ,olleH"

    在进行模糊测试时,你无法预测预期输出,因为你无法控制输入。

    然而,你可以验证 Reverse 函数的某些属性。本模糊测试中正在检查的两个属性是:

    1. 反转字符串两次会保留原始值
    2. 反转后的字符串保持其有效 UTF-8 状态。

    注意单元测试和模糊测试之间的语法差异:

    • 函数以 FuzzXxx 开头,而不是 TestXxx,并取 *testing.F 而不是 *testing.T
    • 你期望看到 t.Run 执行的地方,你会看到 f.Fuzz,它取一个模糊目标函数,其参数是 *testing.T 和要模糊的类型。你单元测试中的输入将使用 f.Add 作为种子语料库输入提供。

    确保已导入新包 unicode/utf8

    package main
    
    import (
        "testing"
        "unicode/utf8"
    )

    将单元测试转换为模糊测试后,是时候再次运行测试了。

    运行代码

    1. 运行模糊测试但不进行模糊,以确保种子输入通过。

      $ go test
      PASS
      ok      example/fuzz  0.013s

      如果你在该文件中有其他测试,并且只想运行模糊测试,你也可以运行 go test -run=FuzzReverse

    2. 使用模糊运行 FuzzReverse,看看是否有任何随机生成的字符串输入会导致失败。这是使用 go test 和一个新标志 -fuzz 来执行的,设置为参数 Fuzz。复制以下命令。

      $ go test -fuzz=Fuzz

      另一个有用的标志是 -fuzztime,它限制模糊测试所花费的时间。例如,在下面的测试中指定 -fuzztime 10s 意味着,只要没有更早发生失败,测试将在 10 秒后默认退出。有关其他测试标志,请参阅 cmd/go 文档中的 此部分

      现在,运行你刚刚复制的命令。

      $ go test -fuzz=Fuzz
      fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
      fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
      fuzz: minimizing 38-byte failing input file...
      --- FAIL: FuzzReverse (0.01s)
          --- FAIL: FuzzReverse (0.00s)
              reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
      
          Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
          To re-run:
          go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
      FAIL
      exit status 1
      FAIL    example/fuzz  0.030s

      模糊测试期间发生了失败,导致问题的输入被写入种子语料库文件,即使没有 -fuzz 标志,下次运行 go test 时也会运行该文件。要查看导致失败的输入,请在文本编辑器中打开写入 testdata/fuzz/FuzzReverse 目录的语料库文件。你的种子语料库文件可能包含不同的字符串,但格式将相同。

      go test fuzz v1
      string("泃")

      语料库文件的第一行表示编码版本。接下来的每一行代表构成语料库条目的每个类型的值。由于模糊目标只接受一个输入,因此版本后只有一个值。

    3. 再次运行 go test,不使用 -fuzz 标志;将使用新的失败种子语料库条目:

      $ go test
      --- FAIL: FuzzReverse (0.00s)
          --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
              reverse_test.go:20: Reverse produced invalid string
      FAIL
      exit status 1
      FAIL    example/fuzz  0.016s

      由于我们的测试失败,是时候调试了。

    修复无效字符串错误

    在本节中,你将调试失败,并修复错误。

    在继续之前,请花点时间思考并尝试自己解决问题。

    诊断错误

    有几种不同的方法可以调试此错误。如果你使用 VS Code 作为文本编辑器,你可以 设置调试器 进行调查。

    在本教程中,我们将有用的调试信息记录到终端。

    首先,考虑 utf8.ValidString 的文档。

    ValidString 报告 s 是否完全由有效的 UTF-8 编码的符文组成。

    当前的 Reverse 函数逐字节反转字符串,这就是我们的问题所在。为了保留原始字符串的 UTF-8 编码符文,我们必须改为逐个符文地反转字符串。

    为了检查为什么输入(在这种情况下是中文字符 )导致 Reverse 在反转时产生无效字符串,你可以检查反转字符串中的符文数量。

    编写代码

    在你的文本编辑器中,将 FuzzReverse 中的模糊目标替换为以下内容。

    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })

    如果发生错误,或带 -v 执行测试,此 t.Logf 行将打印到命令行,这有助于你调试此特定问题。

    运行代码

    使用 go test 运行测试

    $ go test
    --- FAIL: FuzzReverse (0.00s)
        --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
            reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
            reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
    FAIL
    exit status 1
    FAIL    example/fuzz    0.598s

    整个种子语料库使用的字符串中每个字符都是一个字节。然而,像 泃 这样的字符可能需要多个字节。因此,逐字节反转字符串会使多字节字符无效。

    注意: 如果你对 Go 如何处理字符串感到好奇,请阅读博客文章 Go 中的字符串、字节、符文和字符 以获得更深入的理解。

    对错误有了更好的理解后,纠正 Reverse 函数中的错误。

    修复错误

    为了纠正 Reverse 函数,让我们逐个符文而不是逐字节遍历字符串。

    编写代码

    在你的文本编辑器中,将现有的 Reverse() 函数替换为以下内容。

    func Reverse(s string) string {
        r := []rune(s)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r)
    }

    关键区别在于 Reverse 现在迭代字符串中的每个 rune,而不是每个 byte。请注意,这只是一个示例,并不能正确处理 组合字符

    运行代码

    1. 使用 go test 运行测试

      $ go test
      PASS
      ok      example/fuzz  0.016s

      测试现在通过!

    2. 再次使用 go test -fuzz 进行模糊测试,看看是否有任何新错误。

      $ go test -fuzz=Fuzz
      fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
      fuzz: minimizing 506-byte failing input file...
      fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
      --- FAIL: FuzzReverse (0.02s)
          --- FAIL: FuzzReverse (0.00s)
              reverse_test.go:33: Before: "\x91", after: "�"
      
          Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
          To re-run:
          go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
      FAIL
      exit status 1
      FAIL    example/fuzz  0.032s

      我们可以看到,字符串在反转两次后不同于原始字符串。这次输入本身是无效的 unicode。如果我们使用字符串进行模糊测试,这怎么可能?

      让我们再次调试。

    修复双反转错误

    在本节中,你将调试双反转失败并修复错误。

    在继续之前,请花点时间思考并尝试自己解决问题。

    诊断错误

    和以前一样,有几种方法可以调试此失败。在这种情况下,使用 调试器 将是一个很好的方法。

    在本教程中,我们将在 Reverse 函数中记录有用的调试信息。

    仔细查看反转后的字符串以发现错误。在 Go 中,字符串是只读的字节切片,可以包含无效 UTF-8 的字节。原始字符串是包含一个字节 '\x91' 的字节切片。当输入字符串设置为 []rune 时,Go 将字节切片编码为 UTF-8,并用 UTF-8 字符 � 替换该字节。当我们比较替换的 UTF-8 字符和输入字节切片时,它们显然不相等。

    编写代码

    1. 在你的文本编辑器中,将 Reverse 函数替换为以下内容。

      func Reverse(s string) string {
          fmt.Printf("input: %q\n", s)
          r := []rune(s)
          fmt.Printf("runes: %q\n", r)
          for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
              r[i], r[j] = r[j], r[i]
          }
          return string(r)
      }

      这将帮助我们理解将字符串转换为符文切片时出错的原因。

    运行代码

    这次,我们只想运行失败的测试以检查日志。为此,我们将使用 go test -run

    要运行 FuzzXxx/testdata 中的特定语料库条目,你可以向 -run 提供 {FuzzTestName}/{filename}。这在调试时很有帮助。在这种情况下,将 -run 标志设置为失败测试的确切哈希值。从你的终端复制并粘贴唯一哈希值;它将不同于下面的值。

    $ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
    input: "\x91"
    runes: ['�']
    input: "�"
    runes: ['�']
    --- FAIL: FuzzReverse (0.00s)
        --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
            reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
            reverse_test.go:18: Before: "\x91", after: "�"
    FAIL
    exit status 1
    FAIL    example/fuzz    0.145s
    
    既然知道输入是无效的 Unicode,让我们修复 `Reverse` 函数中的错误。
    
    ### 修复错误
    
    为了解决这个问题,如果 `Reverse` 的输入不是有效的 UTF-8,我们返回一个错误。
    
    #### 编写代码
    
    1. 在你的文本编辑器中,将现有的 `Reverse` 函数替换为以下内容。
    
       ```go
       func Reverse(s string) (string, error) {
           if !utf8.ValidString(s) {
               return s, errors.New("input is not valid UTF-8")
           }
           r := []rune(s)
           for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
               r[i], r[j] = r[j], r[i]
           }
           return string(r), nil
       }

    如果输入字符串包含无效的 UTF-8 字符,此更改将返回错误。

    1. 由于 Reverse 函数现在返回错误,修改 main 函数以忽略额外的错误值。将现有的 main 函数替换为以下内容。

      func main() {
          input := "The quick brown fox jumped over the lazy dog"
          rev, revErr := Reverse(input)
          doubleRev, doubleRevErr := Reverse(rev)
          fmt.Printf("original: %q\n", input)
          fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
          fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
      }

      这些对 Reverse 的调用应返回 nil 错误,因为输入字符串是有效的 UTF-8。

    2. 你需要导入 errorsunicode/utf8 包。 main.go 中的导入语句应如下所示。

      import (
          "errors"
          "fmt"
          "unicode/utf8"
      )
    3. 修改 reverse_test.go 文件以检查错误,如果生成错误则跳过测试。

      func FuzzReverse(f *testing.F) {
          testcases := []string {"Hello, world", " ", "!12345"}
          for _, tc := range testcases {
              f.Add(tc)  // 使用 f.Add 提供种子语料库
          }
          f.Fuzz(func(t *testing.T, orig string) {
              rev, err1 := Reverse(orig)
              if err1 != nil {
                  return
              }
              doubleRev, err2 := Reverse(rev)
              if err2 != nil {
                   return
              }
              if orig != doubleRev {
                  t.Errorf("Before: %q, after: %q", orig, doubleRev)
              }
              if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                  t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
              }
          })
      }

      你也可以调用 t.Skip() 来停止该模糊输入的执行,而不是返回。

    运行代码

    1. 使用 go test 运行测试

      $ go test
      PASS
      ok      example/fuzz  0.019s
    2. 使用 go test -fuzz=Fuzz 进行模糊测试,几秒后使用 ctrl-C 停止模糊测试。模糊测试将一直运行,直到遇到失败输入,除非你传递 -fuzztime 标志。默认情况下,如果没有发生失败,它将永远运行,并且可以用 ctrl-C 中断。

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
    fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
    fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
    fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
    ...
    fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
    ^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
    PASS
    ok      example/fuzz  228.000s
    1. 使用 go test -fuzz=Fuzz -fuzztime 30s 进行模糊测试,如果没有发现失败,将在 30 秒后退出。

      $ go test -fuzz=Fuzz -fuzztime 30s
      fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
      fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
      fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
      fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
      fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
      fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
      fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
      fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
      fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
      fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
      fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
      fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
      fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
      PASS
      ok      example/fuzz  31.025s

      模糊测试通过!

      除了 -fuzz 标志外,go test 还添加了几种新标志,可以在 文档 中查看。

      有关模糊测试输出中使用的术语的更多信息,请参阅 Go 模糊测试。例如,“new interesting” 指的是扩展现有模糊测试语料库代码覆盖率的输入。随着模糊测试开始,“new interesting” 输入的数量可以预期会急剧增加,随着发现新的代码路径而多次激增,然后随着时间推移而减少。

    结论

    干得好!你刚刚介绍了 Go 中的模糊测试。

    下一步是选择你代码中的一个函数进行模糊测试,并尝试一下!如果模糊测试在你的代码中发现了一个 bug,考虑将其添加到 奖杯案例

    如果你遇到任何问题或有功能建议,提交问题

    有关该功能的讨论和一般反馈,你也可以参与 Gophers Slack 中的 #fuzzing 频道

    有关进一步阅读,请查看 go.dev/security/fuzz 上的文档。

    完整代码

    --- main.go ---

    package main
    
    import (
        "errors"
        "fmt"
        "unicode/utf8"
    )
    
    func main() {
        input := "The quick brown fox jumped over the lazy dog"
        rev, revErr := Reverse(input)
        doubleRev, doubleRevErr := Reverse(rev)
        fmt.Printf("original: %q\n", input)
        fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
        fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
    }
    
    func Reverse(s string) (string, error) {
        if !utf8.ValidString(s) {
            return s, errors.New("input is not valid UTF-8")
        }
        r := []rune(s)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r), nil
    }

    --- reverse_test.go ---

    package main
    
    import (
        "testing"
        "unicode/utf8"
    )
    
    func FuzzReverse(f *testing.F) {
        testcases := []string{"Hello, world", " ", "!12345"}
        for _, tc := range testcases {
            f.Add(tc) // 使用 f.Add 提供种子语料库
        }
        f.Fuzz(func(t *testing.T, orig string) {
            rev, err1 := Reverse(orig)
            if err1 != nil {
                return
            }
            doubleRev, err2 := Reverse(rev)
            if err2 != nil {
                return
            }
            if orig != doubleRev {
                t.Errorf("Before: %q, after: %q", orig, doubleRev)
            }
            if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
            }
        })
    }

    返回顶部