通过一个例子,快速了解其内置包依赖管理工具、测试工具、显示的异常处理方式等知识。

【此例子可在 golang 官网上找到】

简单的 Hello World!

新建一个文件 hello.go。 文件名以 .go 结尾。

文件内容如下

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

引用了标准库中的 fmt 包的 Println 方法,在标准输出打印 Hello, World! 信息。

在命令行中进入对应的目录,执行指令

➜  go run hello.go

Hello, World!

这是一个最简单的 go 程序,只有最简单的输出功能。 实际项目中的 go 程序往往依赖外部的库来提供更多功能。 官方文档中,使用一个稍微复杂的例子来演示引用外部的库/包。


引入外部依赖

将上面的程序稍作改变,引入 rsc.io/quote 包。

package main

import "fmt"

import "rsc.io/quote"

func main() {
    fmt.Println(quote.Go())
}

这里引入了一个外部的包,注意与标准库的包名的区别。

为了运行这段程序,需要额外一步操作。 执行以下命令来创建 go.mod 文件,用来追踪程序中的依赖关系。

➜  go mod init hello

go: creating new go.mod: module hello

执行程序:

➜  go run hello.go
go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
Don't communicate by sharing memory, share memory by communicating.

➜  go run hello.go
Don't communicate by sharing memory, share memory by communicating.

可以看到,两次执行的输出有所差异。 第一次执行时,自动下载了依赖的库。 外部库的版本和摘要保存在 go.sum 文件中。

内置的依赖管理工具

go 是一个内置了依赖管理工具的语言。 这是一个很有用的功能。

其它语言大都需要外部工具来进行以来管理,如 Java 的 Maven,Ruby 的 rubygems,Javascript 的 npm 包,iOS 生态圈的 CocoaPods 等。 go 语言作为后起之秀,直接考虑到依赖管理在工程中的重要作用。 开发者可以直接用标准的模式方法来发布软件包供其他开发者使用,这对于 go 生态圈的构建起到重要的积极作用。


创建自定义 module 的方法

新建一个目录(greetings/)用于创建 module。

进入目录,执行以下指令;

go module init example.com/greetings

该指令创建了 go.mod 文件。 此文件中包含模块的信息,程序其它部分引用时会用到这些信息。 如果模块中引入了其它外部模块的包,也会包含对应模块的版本信息。

创建 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)
    // var message string
    // message = fmt.Sprintf("Hi, %v. Welcome!", name)
    return message
}

这里首先声明了一个名为 greetings 的包。 然后声明了一个 Hello 函数,注意函数的输入参数类型,输出类型。 Hello 函数名的首字母如果是大写,则可以被包外调用,这是 go 语言的一种约定规则。 在函数中,声明并同时初始化一个变量 message(用符号 :=),此写法等同于注释中的另一种写法。


调用自定义 module 的方法

创建一个新的目录 hello/,并在其中创建文件 hello.go,内容如下:

package main

import (
    "fmt"

    "example.com/greetings"
)

func main() {
    // Get a greeting message and print it.
    message := greetings.Hello("Lax")
    fmt.Println(message)
}

创建 go.mod 文件:

go mod init hello

修改 go.mod 文件,末尾追加一行:

replace example.com/greetings => ../greetings

执行 go build 指令。

➜  go build
go: found example.com/greetings in example.com/greetings v0.0.0-00010101000000-000000000000

再次查看 go.mod 的内容,会发现末尾又追加了一行内容。

此时,目录中出现了一个新的文件 ./hello。 在命令行中执行此文件:

➜  ./hello 
Hi, Lax. Welcome!

异常处理

修改 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
}

增加了引入标准库中的 errors 包。 增加了一个 error 类型的返回值。 增加了一个判断逻辑,如果输入参数的 name 值为空字符串,则跳过后续的处理,直接返回一个 error 对象。 正常处理逻辑中返回值增加了第二个参数 nil。

修改 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)

    // 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 函数后复制给 message 和 err,其中 err 是报错信息的变量。 Hello 函数的输入是一个空字符串,用于测试。 检测到 err 变量非空值时,使用 log 包的 Fatal 方法执行输出操作,此操作也会使得程序退出执行。

if err != nil

fail-fast 是 go 语言的一个“特征”,通过快速失败抛出异常来保证软件开发的质量。


增加一点随机性

这个例子用到了 go 的 slice 来保存多种备选的输出格式。 slice 类似于其它语言的数组结构,可以动态添加和删除元素。

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 函数,从一系列格式化字符串中随机返回一个格式。 formats 是一个未指定长度的 slice,元素类型是 string。 利用 math/rand 包里的 rand.Intn 方法生成一个随机数,作为随机格式的索引。 增加了一个 init 函数,初始化随机数函数。 init 函数是一类特殊的函数,在程序启动时执行。

➜  go build

➜  ./hello
Great to see you, Lax!

➜  ./hello
Hail, Lax! Well met!

➜  ./hello
Hi, Lax. Welcome!

更多的输入/输出(map 数据结构)

可以利用一次函数调用同时提供多个输出值,一次性获取多个输出结果。 方式是将多个输入值作为 slice 传入函数,并修改输出值的类型为 slice。 在此例子中输出结构没有采用 slice,而是 map(映射),可以获得明确的输入值-输出值的对应关系。 由于输入和输出的类型发生了变化,此时提供一个新的函数 Hellos,在某些部分调用原来的 Hello 函数从而复用代码。

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 函数的输出类型为 map[string]string,代表的是键/值类型均为 string 的 map 结构。 在函数体中使用 make(map[key-type]value-type) 写法,申请 messages 映射的存储空间。 for _, name := range names { } 队 names 参数的每个值进行循环遍历,使用特殊标记 _ 将第一个参数忽略。

更新 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 变量,并传入 Hellos 函数。

执行此程序:

➜  go run hello.go         
map[Darrin:Great to see you, Darrin! Gladys:Hi, Gladys. Welcome! Samantha:Great to see you, Samantha!]

输出内容是 map 结构的简单排列。


增加测试

怎么能少得了测试!

在 greetings/ 目录下创建 greetings_test.go 文件。 文件名以 _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)
    }
}

引入 testing 包。 创建两个测试函数。 测试函数名以 Test 开头,代表一个测试用例。 测试函数的输入参数为 testing 的指针。

TestHelloName 使用正则表达式判断函数返回值中是否包含指定字符串。 TestHelloEmpty 检测输入 name 为空字符串的情况。 如果发生不符合期望的结果,则使用 testing 的 Fatalf 方法输出错误提示信息并结束执行该测试用例。

执行测试:

➜  go test

PASS
ok  	example.com/greetings	0.530s

➜  go test -v

=== RUN   TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN   TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok  	example.com/greetings	0.261s

可以修改代码来人为制造一次测试失败,输出如下:

➜  go test
--- FAIL: TestHelloName (0.00s)
    greetings_test.go:15: Hello("Gladys") = "Hi, %!v(MISSING). Welcome!", <nil>, want match for `\bGladys\b`, nil
FAIL
exit status 1
FAIL	example.com/greetings	0.511s
➜  go test -v
=== RUN   TestHelloName
    greetings_test.go:15: Hello("Gladys") = "Great to see you, %!v(MISSING)!", <nil>, want match for `\bGladys\b`, nil
--- FAIL: TestHelloName (0.00s)
=== RUN   TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
FAIL
exit status 1
FAIL	example.com/greetings	0.329s

添加测试真的很方便。 由于有约定的规则,添加测试代码甚至不需要显示引入要测试的包。


安装到本地目录

将开发的程序进行部署是激动人心的事。 go 命令行提供了直接部署的方法。

先来查看默认会安装到什么位置:

➜  go list -f ''
/Users/Lax/go/bin/hello

此目录默认不在 PATH 环境变量中,可以将其进入 PATH。

export PATH="$PATH:$HOME/go/bin"

安装只需要一行指令:

go install

现在执行 hello:

➜  hello
zsh: command not found: hello

由于更改过 PATH,需要重新打开 terminal 或者执行 source /etc/profile

➜  export PATH="$PATH:$HOME/go/bin"

➜  hello 

map[Darrin:Hi, %!v(MISSING). Welcome! Gladys:Hail, %!v(MISSING)! Well met! Samantha:Great to see you, %!v(MISSING)!]

如果希望安装到其它目录,可以设置调整 GOBIN 变量:

go env -w GOBIN=$HOME/bin

总结

以上示例以一个简单的完整的 go 程序的编写、开发、测试和安装步骤,作为 go 开发入门到参考。


参考文档