Post

Go语言入门教程

Go语言入门教程

1.简介

Go语言是由Google开发的开源编程语言,以其简洁高效的语法著称。Go内置并发支持(goroutine和channel),编译速度快,特别适合构建高并发网络服务和云原生应用。

2.安装

下载地址:https://go.dev/dl/

安装指引:https://go.dev/doc/install

对于Windows系统,下载MSI安装包(例如go1.25.6.windows-amd64.msi)。安装程序会自动将安装目录中的bin目录添加到Path环境变量。

对于Linux系统,下载.tar.gz压缩包(例如go1.25.6.linux-amd64.tar.gz)。将其解压到/usr/local目录,之后将/usr/local/go/bin添加PATH环境变量。

1
2
tar -C /usr/local -xzf go1.25.6.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

验证安装成功:

1
2
$ go version
go version go1.25.6 windows/amd64

Go语言源文件的扩展是.go。在任意目录下创建一个文件hello.go,内容如下:

1
2
3
4
5
6
7
package main

import "fmt"

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

使用go命令执行上面的代码,输出结果如下:

1
2
$ go run hello.go
Hello, World!

3.入门

Tutorial: Get started with Go

本教程将简要介绍Go语言,包括:

  • 编写简单的 “Hello, world” 程序
  • 使用go命令运行代码
  • 调用外部模块的函数

3.1 Hello world

1.打开一个命令行窗口并切换到主目录。

2.创建一个helloworld目录。

1
2
mkdir helloworld
cd helloworld

3.开启依赖管理。

代码中依赖的其他模块由项目根目录中名为go.mod的文件定义(类似于Python的requirements.txt)。

运行go mod init命令将创建go.mod文件并开启依赖管理,需要指定模块名称。名称是模块路径(通常是仓库位置,例如github.com/mymodule),详见Managing dependencies

在本教程中,使用example/hello作为模块名称:

1
2
$ go mod init example/hello
go: creating new go.mod: module example/hello

4.使用文本编辑器创建一个文件hello.go。

5.将以下代码粘贴到hello.go并保存(注意缩进使用Tab而不是空格):

1
2
3
4
5
6
7
package main

import "fmt"

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

这段Go代码

  • 声明了一个main包(包由同一个目录下的所有文件组成)。
  • 导入了fmt包,包含格式化文本和控制台打印函数。这个包是标准库的一部分。
  • 实现了一个main()函数用于向控制台打印消息。当运行main包时将默认执行main()函数。

6.运行代码。

1
2
$ go run .
Hello, World!

go run命令用于编译并运行Go程序。可以使用go help命令查看所有命令的列表。

3.2 调用外部包

你的代码可以调用由其他人编写的包中的函数。

1.查找外部包

  • 访问 https://pkg.go.dev/ ,搜索 “quote” 。
  • 在搜索结果中找到rsc.io/quote包,并点击 “Other major versions” 中的v1
  • Documentation - Index一节列出了可以调用的函数。这里使用Go()函数。
  • 在页面顶端可以看到quote包包含在rsc.io/quote模块中。

注:

  • pkg.go.dev是一个可以搜索Go包的网站,类似于Python的pypi.org。
  • Go的“包”和“模块”的含义与Python不同。在Python中,模块=文件,包=目录;在Go中,模块=项目,包=同一个目录下的所有文件。

2.在代码中导入rsc.io/quote包并调用其中的Go()函数。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

import "rsc.io/quote"

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

3.添加模块依赖。

1
$ go mod tidy

go mod tidy命令会自动更新go.mod和go.sum文件(用于认证模块,详见Authenticating modules),添加缺失的依赖,删除无用的依赖,并自动下载依赖的模块。

注:

  • 模块会被下载到$GOPATH/pkg/mod目录。环境变量GOPATH默认为$HOME/go,可通过go env命令查看。
  • go mod tidy命令可能会报错:
1
2
go: example/hello imports
        rsc.io/quote: module rsc.io/quote: Get "https://proxy.golang.org/rsc.io/quote/@v/list": dial tcp 142.251.45.145:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

这是因为默认的代理proxy.golang.org无法访问(由环境变量GOPROXY指定)。解决方法:使用代理服务goproxy.io,只需设置环境变量GOPROXY即可:

1
$ go env -w GOPROXY=https://goproxy.io,direct

再次尝试即可下载成功:

1
2
3
4
5
6
$ go mod tidy
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
  • 如果使用GoLand,需要开启Go模块集成:打开设置 - Go - Go Modules,勾选Enable Go modules integration。

4.运行代码

1
2
$ go run .
Don't communicate by sharing memory, share memory by communicating.

完整代码:helloworld/hello.go

4.模块

在本教程中,将创建两个模块。第一个是库,可以被其他库或应用程序导入。第二个是应用程序,将使用第一个模块。

4.1 创建模块

Tutorial: Create a Go module

Go的代码组织成,包组织成模块。模块定义了运行代码所需的依赖,包括Go版本以及依赖的其他模块。

1.在主目录(或其他任意目录)中创建一个greetings模块。

1
2
3
mkdir greetings
cd greetings
go mod init example.com/greetings

2.创建一个文件greetings.go,内容如下:

1
2
3
4
5
6
7
8
9
10
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中,名字以大写字母开头的函数可以被其他包中的函数调用,这叫做导出名字(exported name)。

function-syntax

  • 在函数Hello()中声明了一个变量message来存放问候语。运算符:=是在一行内声明和初始化变量的快捷方式,等价于:
1
2
var message string
message = fmt.Sprintf("Hi, %v. Welcome!", name)
  • 使用标准库fmt包的Sprintf()创建问候语消息(类似于C语言的sprintf()函数)。第一个参数是格式字符串,使用name的值替换%v(格式字符串语法参见Printing)。
  • 返回格式化后的问候语。

下一步将从另一个模块调用这个函数。

4.2 调用模块

Call your code from another module

在本节中,将编写一个可执行程序,并调用greetings模块中的函数。

1.在greetings所在目录中创建另一个模块hello

1
2
3
4
cd ..
mkdir hello
cd hello
go mod init example.com/hello

目录结构如下:

1
2
3
4
5
6
home/
    greetings/
        go.mod
        greetings.go
    hello/
        go.mod

2.在hello目录中创建一个文件hello.go,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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包中。
  • 导入了两个包:fmtexample.com/greetings,从而代码可以调用这些包中的函数。
  • 调用greetings包的Hello()函数获取问候语,然后调用fmt包的Println()函数将其打印到控制台。

3.在本地找到greetings模块。

上面的代码导入了example.com/greetings模块。默认情况下,Go会从这个URL查找并下载该模块。但目前该模块还未发布,因此需要让hello模块在本地文件系统找到greetings模块。

为此,使用go mod edit命令编辑hello模块:

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

该命令将模块路径example.com/greetings重定向到本地目录../greetings。执行该命令后,hello目录中的go.mod文件将增加一个replace指令:

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

接着,执行go mod tidy命令,同步hello模块的依赖:

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

执行该命令后,hello模块的go.mod文件将再增加一个require指令:

1
require example.com/greetings v0.0.0-00010101000000-000000000000

模块路径后面的数字是伪版本号。为了引用已发布的模块,go.mod文件应该省略replace指令并使用带有真实版本号的require指令,例如

1
require example.com/greetings v1.1.0

关于版本号详见Module version numbering

4.运行代码,在hello目录中执行

1
2
$ go run .
Hi, Gladys. Welcome!

完整代码:

4.3 错误处理

Return and handle an error

处理错误是可靠代码的一个基本特征。在本节中,将在greetings模块中添加返回错误的代码,并在调用代码中处理错误。

1.修改greetings/greetings.go,如果name为空则返回错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。调用者会检查第二个值以查看是否发生了错误。
  • 导入了标准库errors包,以便使用errors.New()函数。
  • 添加了一个if语句以检查参数,如果参数无效(name为空字符串)则返回一个错误。errors.New()函数返回一个具有给定错误消息的error
  • 成功返回时第二个值为nil(表示没有错误),这样调用者就能知道函数成功了。

2.修改hello/hello.go,处理Hello()函数返回的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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()的两个返回值分别赋给变量。
  • 将调用Hello()的参数改为空字符串,以便测试错误处理代码。
  • 如果错误不是nil,使用标准库log包的Fatal()函数打印错误信息并终止程序。

3.在hello目录中运行代码:

1
2
3
$ go run .
greetings: empty name
exit status 1

这就是Go中常见的错误处理方式:将错误作为返回值,由调用者检查(Go没有异常和try语句)。

完整代码:

4.4 返回随机问候语——使用切片

Return a random greeting

在本节中将修改代码,返回几个预定义问候语中的随机一个,而不是每次返回固定的问候语。为此,要使用Go的切片(slice)。

切片类似于数组,但大小会随着添加和删除元素动态变化。关于切片详见Go slices。(注:Go的切片相当于Python的list,与Python的切片不是一回事)

1.修改greetings/greetings.go,添加一个包含三条问候语消息的切片,然后随机返回其中一条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package greetings

import (
	"errors"
	"fmt"
	"math/rand"
)

// 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")
	}

	// Create a message using a random format.
	message := fmt.Sprintf(randomFormat(), name)
	return message, nil
}

// 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()中,声明了一个切片formats,包含三个格式字符串。在声明切片时,省略方括号中的大小,例如[]string。这意味着底层数组的大小可以动态改变。
  • 使用math/rand包中的Intn()函数生成一个[0, 3)之间的随机数,用于从切片中选择一项。
  • Hello()中,将固定问候语改为调用randomFormat()

2.将hello/hello.go中调用Hello()函数的参数恢复为非空值。

1
message, err := greetings.Hello("Gladys")

3.在hello目录中运行代码。多次运行,注意到问候语会变化。

1
2
3
4
5
6
7
8
$ go run .
Great to see you, Gladys!

$ go run .
Hi, Gladys. Welcome!

$ go run .
Hail, Gladys! Well met!

完整代码:

4.5 为多个人返回问候语——使用映射

Return greetings for multiple people

在本节中,将支持为多个人分别返回问候语。换句话说,向函数传递一组名字(使用切片),返回每个人对应的问候语。为此,需要使用映射(map)。

但是,直接修改函数Hello()的参数类型会改变函数的签名。如果你已经发布了example.com/greetings模块并且其他人已经编写了调用Hello()的代码,这种改变将破坏他们的程序。在这种情况下,更好的选择是编写一个具有不同名称的新函数,以保持旧函数向后兼容

1.修改greetings/greetings.go,添加一个函数Hellos()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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
}

在这段代码中

  • 添加了一个Hellos()函数,其参数是一个切片而不是单个string,返回类型从string改为map[string]string,从而可以返回名字到问候语的映射。
  • 让新的Hellos()函数调用已有的Hello()函数,这有助于减少重复。
  • 创建了一个映射messages,将每个名字(作为键)关联到生成的问候语(作为值)。在Go中,使用语法make(map[KeyType]ValueType)初始化一个映射。关于映射详见Go maps in action
  • 遍历函数接收的names切片,检查每一个都非空,然后将每个名字关联一条消息。在这个for循环中,range返回两个值:当前项的索引和值的拷贝。这里不需要索引,因此使用空白标识符(_)忽略它。(注:Go的关键字range类似于Python的enumerate()函数)

2.在hello/hello.go中调用Hellos()函数,传递一个名字的切片,然后打印返回的映射。

1
2
3
4
5
6
7
8
9
10
11
12
// A slice of names.
names := []string{"Gladys", "Samantha", "Darrin"}

// Request greeting messages for the names.
messages, err := greetings.Hellos(names)
// 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 map of messages to the console.
fmt.Println(messages)

3.运行代码,将输出名字/消息映射的字符串表示。

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

完整代码:

4.6 单元测试

Add a test

本节将为Hello()函数添加测试。Go提供了对单元测试的内置支持:使用命名约定、testing包和go test命令可以快速编写和执行测试。

1.在greetings目录中,创建一个名为greetings_test.go的文件。以 _test.go 结尾的文件名告诉go test命令这个文件包含测试函数。

2.文件greetings_test.go的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package greetings

import (
	"regexp"
	"testing"
)

// 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(name)
	if !want.MatchString(msg) || err != nil {
		t.Errorf(`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.Errorf(`Hello("") = %q, %v, want "", error`, msg, err)
	}
}

在这段代码中

  • 创建了两个测试函数来测试greetings.Hello(),与被测函数在同一个包中。
    • TestHelloName()使用一个名字调用Hello(),应该返回合法的消息。如果函数返回了一个错误或者不符合预期的消息(不包含传入的名字),则测试失败。
    • TestHelloEmpty()用一个空字符串调用Hello(),应该返回错误。如果函数返回了非空字符串或者未返回错误,则测试失败。
  • 测试函数名的形式为TestName,其中Name表示特定测试的信息(如被测函数、测试点)。测试函数接受一个指向testing.T类型的指针参数,可以使用这个参数的方法报告错误和输出日志。

3.在greetings目录中,使用go test命令执行测试。可以添加-v标志获得详细输出,这将列出所有测试及其结果。

1
2
3
4
5
6
7
8
9
10
11
$ go test 
PASS
ok      example.com/greetings   0.038s

$ go test -v
=== RUN   TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN   TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok      example.com/greetings   0.036s

4.使测试失败。

测试函数TestHelloName()检查Hello()的返回值包含参数指定的名字。为了查看失败的测试结果,修改Hello()函数(假设出现了错误):

1
2
// message := fmt.Sprintf(randomFormat(), name)
message := fmt.Sprint(randomFormat())

5.再次运行测试,TestHelloName()应该会失败。

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

完整代码:greetings/greetings_test.go

4.7 编译和安装应用

Compile and install the application

go run命令是编译和运行程序的快捷方式,但它不会保留可执行文件(生成在临时目录中,运行后自动删除)。本节将介绍另外两个命令:

1.打开命令行,在hello目录中执行go build命令,将代码编译成可执行文件。

2.运行生成的可执行文件hello(或hello.exe)。

在Linux或Mac上:

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

在Windows上:

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

现在已经把应用程序编译为可执行文件,但是命令行需要位于可执行文件所在目录,或者指定完整路径才能运行它。接下来将安装可执行文件,以便不指定路径(即在任意目录中直接输入hello)即可运行。

3.使用go list命令查看安装路径。

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

例如,该命令可能输出 /home/gopher/go/bin/hello ,表示安装路径为 /home/gopher/go/bin 。

注:安装路径由环境变量GOBIN指定,默认为$GOPATH/bin。

4.将安装路径添加到PATH环境变量,这样无需指定文件位置即可运行程序。

在Linux或Mac上:

1
export PATH=$PATH:/home/gopher/go/bin

在Windows上:

1
set PATH=%PATH%;C:\Users\gopher\go\bin

5.运行go install命令编译并安装hello包。

1
go install

6.打开一个新的命令行窗口,在其他目录中直接输入hello即可运行程序。

1
2
$ hello
map[Darrin:Hail, Darrin! Well met! Gladys:Great to see you, Gladys! Samantha:Hail, Samantha! Well met!]
This post is licensed under CC BY 4.0 by the author.