创建 Go 模块

介绍模块、函数、错误处理、数组、map、单元测试和编译安装.


创建一个其他人可以使用的模块

在一个模块中,您为一组离散且有用的功能收集一个或多个相关包。例如,您可能会创建一个包含具有财务分析功能的包的模块,以便其他编写财务应用程序的人可以使用您的工作。有关开发模块的更多信息,请参阅 开发和发布模块

Go 代码被分组到包中,包被分组到模块中。您的模块指定了运行代码所需的依赖项,包括 Go 版本和它所需的一组其他模块。

  1. 为 Go 模块代码创建一个目录 greetings

    mkdir greetings
    cd greetings
    
  2. 使用命令 go mod init 启动您的模块 。

    运行 go mod init 命令,会生成你的模块路径——example.com/greetings. 如果您发布一个模块,这必须是 Go 工具可以从中下载您的模块的路径。那将是您的代码存储库。

    有关使用模块路径命名模块的更多信息,请参阅 管理依赖项

    $ go mod init example.com/greetings
    go: creating new go.mod: module example.com/greetings
    
  3. 创建代码文件 greetings.go,粘贴以下代码并保存文件。

    package greetings
    
    import "fmt"
    
    // Hello returns a greeting for the named person.
    func Hello(name string) string {
        // Return a greeting that embeds the name in a message.
        message := fmt.Sprintf("Hi, %v. Welcome!", name)
        return message
    }
    

    这是您的模块的第一个代码。它会向任何请求的呼叫者返回问候语。在此代码中,您:

    • 声明一个greetings包来收集相关功能。
    • 实现一个Hello函数来返回问候语。

      该函数接受一个类型为 string 的参数 name。该函数还返回一个string. 在 Go 中,名称以大写字母开头的函数可以被不在同一个包中的函数调用。这在 Go 中称为导出名称。有关导出名称的更多信息,请参阅 Go tour 中的导出名称

    • 声明一个message变量来保存你的问候。

      在 Go 中,:=运算符是在一行中声明和初始化变量的快捷方式(Go 使用右侧的值来确定变量的类型)。从长远来看,您可能已将其写为:

      var message string
      message = fmt.Sprintf("Hi, %v. Welcome!", name)
      
    • 使用fmt包的Sprintf功能来创建问候消息。第一个参数是格式字符串,Sprintf将参数name的值替换为格式动词%v。插入参数name的值完成了问候文本。

    • 将格式化的问候语文本返回给呼叫者。

在另一个模块中调用代码

  1. 为 Go 模块创建一个目录 hello,在这个文件夹写调用者的代码。

    cd ..
    mkdir hello
    cd hello
    

    目录结构如下:

    <home>/
     |-- greetings/
     |-- hello/
    
  2. 启用依赖项跟踪

    运行go mod init,为模块启用依赖项跟踪

    $ go mod init example.com/hello
    go: creating new go.mod: module example.com/hello
    
  3. 创建代码文件 hello.go 并粘贴以下代码

    package main
    
    import (
        "fmt"
    
        "example.com/greetings"
    )
    
    func main() {
        // Get a greeting message and print it.
        message := greetings.Hello("Gladys")
        fmt.Println(message)
    }
    

    在此段代码中,您: - 声明一个 main 包。在Go中,作为应用程序执行的代码必须在 main 包中。 - 导入两个包 example.com/greetingsfmt 。这使得您的代码可以访问这些包的函数。导入 example.com/greetings (您之前创建的模块中的包) 使您可以访问 Hello 函数。导入 fmt ,具有处理输入和输出文本的功能,例如将文本打印到控制台。

  4. 编辑 example.com/hello 模块以使用您的本地 example.com/greetings 模块。

    对于生产用途,您将 example.com/greetings 从其存储库发布模块(使用反映其发布位置的模块路径),Go 工具可以在其中找到它并下载它。目前,由于您尚未发布该模块,您需要调整该 example.com/hello 模块,以便它可以在您的本地文件系统上找到 example.com/greetings 代码。

    为此,请使用 go mod edit command 命令编辑 example.com/hello 模块以将 Go 工具从其模块路径(模块所在的位置)重定向到本地目录(模块所在的位置)。

    1. hello 目录执行以下命令:

      $ go mod edit -replace example.com/greetings=../greetings
      

      该命令指定 example.com/greetings 应替换 ../greetings 作为依赖项的定位。运行命令后, hello 目录中的 go.mod 文件应该包含一个 replace指令

      module example.com/hello
      
      go 1.16
      
      replace example.com/greetings => ../greetings
      
    2. 在 hello 目录中下,运行 go mod tidy 命令同步模块 example.com/hello 的依赖关系,添加代码所需但尚未在模块中跟踪的那些。

      $ go mod tidy
      go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000
      

      命令完成后,example.com/hello 模块的 go.mod 文件应如下所示:

      module example.com/hello
      
      go 1.16
      
      replace example.com/greetings => ../greetings
      
      require example.com/greetings v0.0.0-00010101000000-000000000000
      

      该命令在 greetings 目录中找到了本地代码,然后添加了一个require 指令来指定example.com/hello 依赖 example.com/greetings。您在 hello.go 中导入 greetings 包时创建了此依赖项。

      模块路径后面的数字是一个伪版本号 ——一个生成的数字用来代替语义版本号(模块还没有)。

      要引用已发布的模块,go.mod 文件通常会省略replace指令并使用 require末尾带有标记版本号的指令。

      require example.com/greetings v1.1.0
      
  5. hello 目录下,运行代码以确认其工作。

    $ go run .
    Hi, Gladys. Welcome!
    

返回并处理错误

处理错误是可靠代码的基本特征。在下面的教程中,您将添加一些代码以从greetings模块返回错误,然后在调用者中处理它。

  1. 修改 greetings/greetings.go 如下代码。

    如果您不知道该问候谁,那么发送问候是没有意义的。如果名称为空,则向调用者返回错误。将以下代码复制到 greetings.go 并保存文件。

    package greetings
    
    import (
        "errors"
        "fmt"
    )
    
    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return "", errors.New("empty name")
        }
    
        // If a name was received, return a value that embeds the name
        // in a greeting message.
        message := fmt.Sprintf("Hi, %v. Welcome!", name)
        return message, nil
    }
    

    在这段代码中,您:

    • 更改函数,使其返回两个值: 一个 string 和一个 error。您的调用者将检查第二个值以查看是否发生错误。(任何 Go 函数都可以返回多个值。有关更多信息,请参阅 Effective Go。)
    • 导入 Go 标准库 errors 包,以便您可以使用它的 errors.New 函数
    • 添加一条if语句以检查无效请求(名称应为空字符串)并在请求无效时返回错误。该errors.New函数返回一个 error内部带有您的消息。
    • 添加nil(意味着没有错误)作为成功返回的第二个值。这样,调用者就可以看到函数成功了。
  2. hello/hello.go 文件中,处理 Hello 函数现在返回的错误以及非错误值。

    package main
    
    import (
        "fmt"
        "log"
    
        "example.com/greetings"
    )
    
    func main() {
        // Set properties of the predefined Logger, including
        // the log entry prefix and a flag to disable printing
        // the time, source file, and line number.
        log.SetPrefix("greetings: ")
        log.SetFlags(0)
    
        // Request a greeting message.
        message, err := greetings.Hello("")
        // If an error was returned, print it to the console and
        // exit the program.
        if err != nil {
            log.Fatal(err)
        }
    
        // If no error was returned, print the returned message
        // to the console.
        fmt.Println(message)
    }
    

    在这段代码中,您:

    • log 配置为在其日志消息的开头打印命令名称(“greetings:”),不带时间戳或源文件信息。
    • Hello的两个返回值(包括 error)分配给变量。
    • Hello的参数从 Gladys 的名字更改为空字符串,以便您可以尝试错误处理代码。
    • 寻找一个非 nilerror值。在这种情况下继续下去是没有意义的。
    • 使用标准库中的函数log package来输出错误信息。如果出现错误,您可以使用 log 包的 Fatal 函数 来打印错误并停止程序。
  3. hello 目录运行 hello.go 以确认代码有效。

    现在你传入一个空名称,会得到一个错误。

    $ go run .
    greetings: empty name
    exit status 1
    

返回随即问候语

您将添加一个小片段以包含三条问候消息,然后让您的代码随机返回其中一条消息。

为此,您将使用 Go slice(切片)。切片就像一个数组,只是它的大小会随着您添加和删除项目而动态变化。切片是 Go 最有用的类型之一。

有关切片的更多信息,请参阅 Go 博客中的 Go slices

  1. greetings/greetings.go 中,更改您的代码,使其如下所示。

    package greetings
    
    import (
        "errors"
        "fmt"
        "math/rand"
        "time"
    )
    
    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return name, errors.New("empty name")
        }
        // Create a message using a random format.
        message := fmt.Sprintf(randomFormat(), name)
        return message, nil
    }
    
    // init sets initial values for variables used in the function.
    func init() {
        rand.Seed(time.Now().UnixNano())
    }
    
    // randomFormat returns one of a set of greeting messages. The returned
    // message is selected at random.
    func randomFormat() string {
        // A slice of message formats.
        formats := []string{
            "Hi, %v. Welcome!",
            "Great to see you, %v!",
            "Hail, %v! Well met!",
        }
    
        // Return a randomly selected message format by specifying
        // a random index for the slice of formats.
        return formats[rand.Intn(len(formats))]
    }
    

    在此代码中,您:

    • 添加一个randomFormat函数,该函数返回随机选择的问候消息格式。请注意,randomFormat以小写字母开头,使其只能被其自己的包中的代码访问(换句话说,它不会被导出)。
    • randomFormat中,声明formats具有三种消息格式的切片。声明切片时,在括号中省略其大小,如下所示:[]string. 这告诉 Go,切片底层数组的大小可以动态更改。
    • 使用 math/rand 生成一个随机数,用于从切片中选择一个项目。
    • 添加一个init函数以使用当前时间为rand包的随机数种子(seed)。Go在程序启动时自动执行init函数,在全局变量被初始化之后。有关init函数的更多信息,请参阅 Effective Go
    • Hello中,调用randomFormat函数以获取您将返回的消息的格式,然后将格式和 name值一起使用来创建消息。
    • 像以前一样返回消息(或错误)。
  2. hello/hello.go 中,更改您的代码,使其如下所示。

    只需将 Gladys 的名字(或其他名字,如果您愿意的话)作为参数添加到 hello.go 中的Hello函数调用中。

    package main
    
    import (
        "fmt"
        "log"
    
        "example.com/greetings"
    )
    
    func main() {
        // Set properties of the predefined Logger, including
        // the log entry prefix and a flag to disable printing
        // the time, source file, and line number.
        log.SetPrefix("greetings: ")
        log.SetFlags(0)
    
        // Request a greeting message.
        message, err := greetings.Hello("Gladys")
        // If an error was returned, print it to the console and
        // exit the program.
        if err != nil {
            log.Fatal(err)
        }
    
        // If no error was returned, print the returned message
        // to the console.
        fmt.Println(message)
    }
    
  3. 运行 hello.go 以确认代码正常运行。多次运行它,注意问候语发生了变化。

    $ go run .
    Great to see you, Gladys!
    
    $ go run .
    Hi, Gladys. Welcome!
    
    $ go run .
    Hail, Gladys! Well met!
    

回复多人问候

添加对在一个请求中获取多个人的问候的支持。换句话说,处理多值输入,然后将该输入中的值与多值输出配对。为此,您需要将一组名称传递给一个函数,该函数可以为每个名称返回一个问候语。

但是有一个问题。将Hello函数的参数从单个名称更改为一组名称会更改函数的签名。如果您已经发布了example.com/greetings 模块并且用户已经编写了代码调用Hello,那么该更改将破坏他们的程序。

在这种情况下,更好的选择是编写一个具有不同名称的新函数。新函数将采用多个参数。这保留了旧功能以实现向后兼容性。

  1. greetings/greetings.go 中,更改您的代码,使其如下所示。

    package greetings
    
    import (
        "errors"
        "fmt"
        "math/rand"
        "time"
    )
    
    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return name, errors.New("empty name")
        }
        // Create a message using a random format.
        message := fmt.Sprintf(randomFormat(), name)
        return message, nil
    }
    
    // Hellos returns a map that associates each of the named people
    // with a greeting message.
    func Hellos(names []string) (map[string]string, error) {
        // A map to associate names with messages.
        messages := make(map[string]string)
        // Loop through the received slice of names, calling
        // the Hello function to get a message for each name.
        for _, name := range names {
            message, err := Hello(name)
            if err != nil {
                return nil, err
            }
            // In the map, associate the retrieved message with
            // the name.
            messages[name] = message
        }
        return messages, nil
    }
    
    // Init sets initial values for variables used in the function.
    func init() {
        rand.Seed(time.Now().UnixNano())
    }
    
    // randomFormat returns one of a set of greeting messages. The returned
    // message is selected at random.
    func randomFormat() string {
        // A slice of message formats.
        formats := []string{
            "Hi, %v. Welcome!",
            "Great to see you, %v!",
            "Hail, %v! Well met!",
        }
    
        // Return one of the message formats selected at random.
        return formats[rand.Intn(len(formats))]
    }
    

    在此代码中,您:

    • 添加一个Hellos函数,其参数是名称切片而不是单个名称。此外,您将其返回类型之一从一个 string更改为一个 map以便您可以返回映射到问候消息的名称。
    • 让新Hellos函数调用现有 Hello函数。这有助于减少重复,同时也保留这两个功能。
    • 创建一个messages map 映射以将每个接收到的名称(作为键)与生成的消息(作为值)相关联。在 Go 中,您使用以下语法初始化map: make(map[key-type]value-type). 您有函数Hellos将此映射返回给调用者。有关map的更多信息,请参阅Go 博客上的 Go maps in action
    • 遍历您的函数收到的名称,检查每个名称是否具有非空值,然后将一条消息与每个关联。在这个 for循环中,range返回两个值:循环中当前项目的索引和项目值的副本。您不需要索引,因此您使用 Go 空白标识符(下划线)来忽略它。有关更多信息,请参阅 Effective Go 中的空白标识符
  2. 在您的 hello/hello.go 调用代码中,传递一段名称,然后打印您返回的名称/消息映射的内容。

    package main
    
    import (
        "fmt"
        "log"
    
        "example.com/greetings"
    )
    
    func main() {
        // Set properties of the predefined Logger, including
        // the log entry prefix and a flag to disable printing
        // the time, source file, and line number.
        log.SetPrefix("greetings: ")
        log.SetFlags(0)
    
        // A slice of names.
        names := []string{"Gladys", "Samantha", "Darrin"}
    
        // Request greeting messages for the names.
        messages, err := greetings.Hellos(names)
        if err != nil {
            log.Fatal(err)
        }
        // If no error was returned, print the returned map of
        // messages to the console.
        fmt.Println(messages)
    }
    

    通过这些更改,您可以:

    • 创建一个names变量作为包含三个名称的切片类型。
    • names变量作为参数传递给 Hellos函数。
  3. 切换到包含 hello/hello.go 的目录,然后使用go run确认代码有效。

    输出应该是地图的字符串表示形式,将名称与消息相关联,如下所示:

    $ go run .
    map[Darrin:Hail, Darrin! Well met! Gladys:Hi, Gladys. Welcome! Samantha:Hail, Samantha! Well met!]
    

本主题介绍了用户表示名称/值对的map映射。它还引入了通过为模块中增加新函数或更改函数实现新功能来保持向后兼容性的想法。有关向后兼容性的更多信息,请参阅 保持模块兼容

添加测试

现在您已经将代码放到了一个稳定的地方,添加一个测试,在开发期间测试您的代码可能会暴露在您进行更改时发现的错误。

Go 对单元测试的内置支持使您可以更轻松地进行测试。具体来说,使用命名约定、Go 的testing包和go test命令,您可以快速编写和执行测试。

  1. 在 greetings 目录中,创建一个名为 greetings_test.go 的文件。

    以 _test.go 结尾的文件名告诉go test命令该文件包含测试函数。

  2. 在 greetings_test.go 中,粘贴以下代码并保存文件。

    package greetings
    
    import (
        "testing"
        "regexp"
    )
    
    // TestHelloName calls greetings.Hello with a name, checking
    // for a valid return value.
    func TestHelloName(t *testing.T) {
        name := "Gladys"
        want := regexp.MustCompile(`\b`+name+`\b`)
        msg, err := Hello("Gladys")
        if !want.MatchString(msg) || err != nil {
            t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want)
        }
    }
    
    // TestHelloEmpty calls greetings.Hello with an empty string,
    // checking for an error.
    func TestHelloEmpty(t *testing.T) {
        msg, err := Hello("")
        if msg != "" || err == nil {
            t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
        }
    }
    

    在此代码中,您:

    • 在与您正在测试的代码相同的包中实现测试函数。
    • 创建两个测试函数来测试greetings.Hello 函数。测试函数名称的格式为TestName,其中Name说明了特定测试。此外,测试函数将指向包 testing 类型 testing.T的指针作为参数。您可以使用此参数的方法来报告和记录您的测试。
    • 实施两个测试:
      • TestHelloName调用Hello函数,传递一个name值,函数应该能够使用该值返回有效的响应消息。如果调用返回错误或意外响应消息(不包括您传入的名称),则使用t参数的 Fatalf 方法将消息打印到控制台并结束执行。
      • TestHelloEmpty使用空字符串调用Hello函数。此测试旨在确认您的错误处理是否有效。如果调用返回非空字符串或没有错误,则使用t参数的Fatalf 方法将消息打印到控制台并结束执行。
  3. 在greetings目录下的命令行,运行 go test 命令 执行测试。

    go test命令在测试文件(名称以 _test.go 结尾)中执行测试函数(名称以 Test 开头)。您可以添加-v标志以获得列出所有测试及其结果的详细输出。

    $ go test
    PASS
    ok      example.com/greetings   0.364s
    
    $ go test -v
    === RUN   TestHelloName
    --- PASS: TestHelloName (0.00s)
    === RUN   TestHelloEmpty
    --- PASS: TestHelloEmpty (0.00s)
    PASS
    ok      example.com/greetings   0.372s
    
  4. 中断greetings.Hello函数以查看失败的测试。

    TestHelloName测试函数检查您指定名称为Hello的函数的返回值。要查看失败的测试结果,请更改greetings.Hello函数以使其不再包含名称。

    greetings/greetings.go 中,粘贴以下代码代替 Hello函数。请注意,突出显示的行会更改函数返回的值,就好像name参数被意外删除了一样。

    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return name, errors.New("empty name")
        }
        // Create a message using a random format.
        // message := fmt.Sprintf(randomFormat(), name)
        message := fmt.Sprint(randomFormat())
        return message, nil
    }
    
  5. 在greetings目录下,运行go test执行测试。

    这一次,在go test没有-v参数的情况下运行。输出将仅包含失败测试的结果,这在您有大量测试时很有用。测试TestHelloName应该失败——TestHelloEmpty仍然通过。

    $ go test
    --- FAIL: TestHelloName (0.00s)
        greetings_test.go:15: Hello("Gladys") = "Hail, %v! Well met!", <nil>, want match for `\bGladys\b`, nil
    FAIL
    exit status 1
    FAIL    example.com/greetings   0.182s
    

编译和安装应用程序

学习几个新go命令。虽然该go run命令是在您进行频繁更改时编译和运行程序的有用快捷方式,但它不会生成二进制可执行文件。

本主题介绍了用于构建代码的两个附加命令:

常用命令

  1. 从 hello 目录中的命令行,运行go build 命令将代码编译为可执行文件。

    $ go build
    
  2. 从 hello 目录中的命令行,运行新的hello 可执行文件以确认代码有效。

    请注意,您的结果可能会有所不同,具体取决于您是否在测试后更改了 greetings.go 代码。

    • 在 Linux 或 Mac 上:

      $ ./hello
      map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
      
    • 在 Windows 上:

      $ hello.exe
      map[Darrin:Great to see you, Darrin! Gladys:Hail, Gladys! Well met! Samantha:Hail, Samantha! Well met!]
      

    您已将应用程序编译为可执行文件,以便可以运行它。但是要在当前目录运行它,您的命令提示符需要位于可执行文件的目录中,或者指定可执行文件的路径。

    接下来,您将安装可执行文件,以便在不指定其路径的情况下运行它。

  3. 发现 Go 安装路径,该go命令将在其中安装当前包。

    您可以通过运行 go list 命令 来发现安装路径, 如下例所示:

    $ go list -f '{{.Target}}'
    

    例如,该命令的输出可能会/home/gopher/bin/hello显示 ,这意味着二进制文件将被安装到 /home/gopher/bin。您将在下一步中需要此安装目录。

  4. 将 Go 安装目录添加到系统的 shell 路径。

    这样,您将能够运行程序的可执行文件,而无需指定可执行文件的位置。

    • 在 Linux 或 Mac 上,运行以下命令:

      $ export PATH=$PATH:/path/to/your/install/directory
      
    • 在 Windows 上,运行以下命令:

      $ set PATH=%PATH%;C:\path\to\your\install\directory
      

    作为替代方案,如果您的 shell 路径中已经有一个目录 $HOME/bin,并且您想在那里安装您的 Go 程序,您可以通过使用 go env 命令设置变量 GOBIN 来更改安装目标:

    $ go env -w GOBIN=/path/to/your/bin
    

    或者

    $ go env -w GOBIN=C:\path\to\your\bin
    
  5. 更新 shell 路径后,运行go install命令编译和安装包。

    $ go install
    
  6. 只需键入其名称即可运行您的应用程序。为了让这个有趣,打开一个新的命令提示符并在其他目录中运行可执行文件名hello

    $ hello
    map[Darrin:Hail, Darrin! Well met! Gladys:Great to see you, Gladys! Samantha:Hail, Samantha! Well met!]