Learning Golang: Hello World!
通过一个例子,快速了解其内置包依赖管理工具、测试工具、显示的异常处理方式等知识。
【此例子可在 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 开发入门到参考。