Go 语言基础

本文包含 Go 语言的基本语法内容,内容参考下列网站:
微软培训 Go - Training
Go 语言数据类型 | 菜鸟教程
Go语言官方库

认识Go和其历史

历史

Go 是 Google 开发的一种编程语言。 它于 2009 年由 Robert Griesemer、Rob Pike 和 Ken Thompson 作为开源项目发布。 从那时起,Go 语言就被用于开发其他众所周知的技术,如 Docker、Kubernetes 和 Terraform。 尽管 Go 语言在服务器端和云软件中广泛使用,但它是一种常规用途语言,具有丰富的不同用例。
Go 语言表现力强,且简单明了。 它在设计时考虑了惯用语言,这使程序员能够高效地编写高效且可靠的代码。 以 Go 语言编写的程序可以在 Unix 系统上运行,例如 Linux 和 macOS,也可以在 Windows 系统上运行。 Go 语言之所以值得注意,部分原因在于它独特的并发机制,使得编写可同时利用多个内核的程序非常容易。 它主要是一种强化静态类型的语言,这意味着变量类型在编译时是已知的。 不过,它确实具有一些动态类型化功能。
根据 TIOBE 索引,Go 是 2009 和 2016 年的年度编程语言。 尽管 Go 在 2016 年达到了顶点,但它仍保持着很好的口碑。 根据年度 Stack Overflow 开发人员调查,Go 仍然是最受欢迎的语言之一。
Go 语言与 C 语言有很多相似之处,它继承了 C 语言语法的许多方面,如控制流语句、基本数据类型、指针和其他元素等。 不过,该语言的语法和语义均超出 C 语言。 它还与 Java、C#、Python 等有相似之处。 一般情况下,Go 语言往往从其他编程语言中借用并调整功能,同时去掉了大部分复杂性。 例如,你可以在 Go 语言中使用一些面向对象的 (OO) 编程功能和设计模式,但并不完全实现整个 OO 范例。 你将在此学习路径的后面部分了解其中的原因。

Go 原则

下面是 Go 编程语言的基本原理优势:

  1. Go 许可证是完全开放源代码的。
  2. Go 程序编译为单独的二进制文件,这样更易于共享和分发。
  3. Go 支持交叉编辑到各种平台和操作系统。
  4. Go 语言致力于使语言变得简单,并用更少的代码行执行更多操作。
  5. 并发是头等概念,使任何函数可以作为轻量级线程运行,而程序员只需少量工作。
  6. Go 语言提供自动内存管理,包括垃圾回收。
  7. 编译和执行速度很快。
  8. Go 语言需要使用所有代码,否则会引发错误。
  9. 有一种官方格式设置可帮助保持项目之间的一致性。
  10. Go 语言具有大量全面标准库,并且可以在不使用第三方依赖项的情况下生成多个应用程序。
  11. Go 保证语言与以前版本的后向兼容性。

用例

  • 系统级应用程序
  • Web 应用程序
  • 云原生应用程序
  • 实用工具和命令行工具
  • 分布式系统
  • 数据库实现

安装Go

linux-CentOS

image.png
第一步,安装 wget

1
yum install -y wget

第二步,下载

1
2
wget -c https://dl.google.com/go/go1.20.1.linux-amd64.tar.gz -O - | sudo tar -xz -C /usr/local

配置环境变量
创建GOPATH文件夹

1
2
cd /usr/local/
mkdir gopath

打开配置文件

1
vi /etc/profile

更改配置信息

1
2
3
4
5
6
7
8
9
# 在/etc/profile最后一行添加,GOPATH路径更换成上面创建的路径
# i 切换更改模式
export GOROOT=/usr/local/go
export GOPATH=/usr/local/gopath
export PATH=$PATH:$GOROOT/bin
export GO111MODULE="on" # 开启 Go moudles 特性
export GOPROXY=https://goproxy.cn,direct # 安装 Go 模块时,国内代理服务器设置
# esc 推出更改模式
# :wq 保存更改后退出

使环境变量立刻生效

1
source /etc/profile

查看版本号

1
go version

查看环境变量

1
go env

Windows

在Windows上安装GO只需要在官网上下载 GO 语言的安装包即可。
下载地址:https://go.dev/dl/
安装完毕之后,在命令行中输入一下命令即可完成通话:

1
go version

配置工作区

1.为所有 Go 项目创建一个顶级文件夹。 例如,C:\Projects\Go
2.打开 PowerShell 提示符,然后运行以下 cmdlet 来设置 $GOPATH环境变量。
<project-folder> 替换为上一步中创建的顶级项目文件夹。

1
[Environment]::SetEnvironmentVariable("GOPATH", "<project-folder>", "User")

3.确认 $GOPATH 变量显示正确的工作区位置。 在新的提示符窗口中,运行以下命令

1
go env GOPATH

输出显示当前工作区位置为顶级项目文件夹:

1
C:\Projects\Go

工作区说明

每个 Go 工作区都包含三个基本文件夹:

  • bin:包含应用程序中的可执行文件。
  • src:包括位于工作站中的所有应用程序源代码。
  • pkg:包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。

例如,工作站文件夹结构树可能与下面的示例类似:

bin/
hello
coolapp
pkg/
github.com/gorilla/
mux.a
src/
github.com/golang/example/
.git/
hello/
hello.go

为工作区创建三个子文件夹:binpkgsrc

Go env 说明

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
35
36
37
38
39
set GO111MODULE=
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\16496\AppData\Local\go-build
set GOENV=C:\Users\16496\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=D:\Code\GoProjects\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=D:\Code\GoProjects
set GOPRIVATE=
set GOPROXY=https://mirrors.aliyun.com/goproxy/
set GOROOT=C:\Program Files\Go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.20.1
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=0
set GOMOD=NUL
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=C:\Users\16496\AppData\Local\Temp\go-build190679691=/tmp/go-build -gno-record-gcc-switches

声明&变量&常量

声明&变量

若要声明变量,需要使用 var 关键字

1
var firstname string

声明 string 类型中的一个名为 firstName 的变量。

1
var firstname. lastname string

通过在变量名称后面添加逗号 ,,就表示你将要声明其他变量。 在这种情况下,前一个语句就声明了 string 类型中的两个变量:firstNamelastName

1
2
3
4
var (
fristname, lastname string
age int
)
1
2
var fristname, lastname string
var age int

初始化操作

1
2
3
4
5
var (
firstname string = "Felix"
lastname string = "cai"
age int = 23
)
1
2
3
4
5
var (
fristname = "Felix"
lastname = "cai"
age = 23
)

当你想要初始化一个变量的时候,是不需要指定其类型的,Go 会自动判断这个变量的类型。Go 将推断出变量 firstName lastName 的类型为 string,并且变量 age 属于 int 类型。

1
2
3
var (
firstname, lastname, age = "Felix", "Cai", 23
)

当然,下面是在 Go 中最常用的赋值方式,冒等号 :=

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
firstname, lastname := "Felix", "Cai"
age := 23
fmt.Println(fristname, lastname, age)
}

请注意 import "fmt" 语句。 我们使用 import 关键字将包的内容引入范围中。 我们要导入“fmt”包,因此可以在代码中使用 Println 方法。 我们将在后面的单元中详细了解此关键字。

请注意,在定义变量名称后,需要在此处加入一个冒号等于号 := 和相应的值。 使用冒号等于号时,要声明的变量必须是新变量。 如果使用冒号等于号并已经声明该变量,将不会对程序进行编译。

声明常量

有时,你需要在代码中加入静态值,这称为常量。 Go 支持使用常量。 用于声明常量的关键字是 const

1
const fristInt = 0

与变量一样,Go 可以通过分配给常量的值推断出类型。 在 Go 中,常量名称通常以混合大小写字母或全部大写字母书写。

1
2
3
4
5
const (
Aconst = 0
BCONST = 1
Cconst = 2
)

iota

iota 是一个关键字!简单理解为常量计数器,特殊常量,可以认为是一个可以被编译器修改的常量。
iotaconst 关键字出现时将被重置为 0 (const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
这里有几个有趣的例子:官网上的两个案例

1
2
3
4
5
6
7
8
9
10
11
12
13
type Weekday int
const (
Sunday Weekday = iota + 1 // iota: 0 ~ Sunday : 1
_ // iota: 1 ~ iota increased
// comments // iota: 1 ~ skip comment
// iota: 1 ~ skip empty line
Monday // iota: 2 ~ Monday : 3
Tuesday // iota: 3 ~ Tuesday : 4
Wednesday // iota: 4 ~ Wednesday : 5
Thursday // iota: 5 ~ Thursday : 6
Friday // iota: 6 ~ Friday : 7
Saturday // iota: 7 ~ Saturday : 8
)

菜鸟教程上的一个例子:

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

import "fmt"
const (
i=1<<iota
j=3<<iota
k
l
)

func main() {
fmt.Println("i=",i)
fmt.Println("j=",j)
fmt.Println("k=",k)
fmt.Println("l=",l)
}

/* 以下为输出:
i= 1
j= 6
k= 12
l= 24
*/

:::info
注:<<n==*(2^n)
:::

注意事项

  • 常量的声明不能使用 :=
  • 变量声明了必须要使用,否则会报错

数据类型

Go 是一种强类型语言。 你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。
Go 有四类数据类型:

  • 基本类型:数字、字符串和布尔值
  • 聚合类型:数组和结构
  • 引用类型:指针、切片、映射、函数和通道
  • 接口类型:接口

基本数据类型

数字

整数

一般来说,定义整数类型的关键字是 int。 但 Go 还提供了 int8int16int32int64 类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int,则大小通常为 32 位。 在 64 位系统上,int 大小通常为 64 位。 但是,此行为可能因计算机而不同。
只有在出于某种原因需要将值表示为无符号数字的情况下,才使用 uint 类型。 此外,Go 还提供 uint8uint16uint32uint64 类型。

1
2
3
4
5
var integer8 int8 = 127
var integer16 int16 = 32767
var integer32 int32 = 2147483647
var integer64 int64 = 9223372036854775807
fmt.Println(integer8, integer16, integer32, integer64)

大多数情况都是使用 int,但需要了解其他整数类型,因为在 Go 中,intint32 不同,即使整数的自然大小为 32 位也是如此。
在 Go 中将值从一种类型转换为另一种类型时,需要显式声明新类型。

rune

有时可能会收到有关 runes 的信息。 rune 只是 int32 数据类型的别名。 它用于表示 Unicode 字符(或 Unicode 码位)。

1
2
3
rune := 'G'
fmt.Println(rune)
// 输出 71 ,它表示 G 的 Unicode 字符

浮点数

Go 提供两种浮点数大小的数据类型:float32float64

1
2
3
var float32 float32 = 2147483647
var float64 float64 = 9223372036854775807
fmt.Println(float32, float64)

可以使用 math 包中提供的 math.MaxFloat32 和 math.MaxFloat64 常量来查找这两种类型的限制

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"math"
)

func main() {
fmt.Println(math.MaxFloat32, math.MaxFloat64)
}
1
2
3
const e = 2.71828
const Avogadro = 6.02214129e23
const Planck = 6.62606957e-34

布尔值

布尔类型仅可能有两个值:truefalse。 你可以使用关键字 bool 声明布尔类型。 Go 与其他编程语言不同。 在 Go 中,不能将布尔类型隐式转换为 0 或 1。 你必须显式执行此操作。

1
var featureFlag bool = true

字符串

关键字 string 用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号"中定义值。 单引号'用于单个字符(以及 runes,正如我们在上一节所述)。

1
2
3
var fristname = "Felix"
lastname := "Cai"
fmt.println(fristname, lastname)

转义字符

  • \n:新行
  • \r:回车符
  • \t:制表符
  • \':单引号
  • \":双引号
  • \\:反斜杠
1
2
var fullName = "Felix\tCai"
fmt.Println(fullName)

默认值

在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。在使用之前,无需检查变量是否已初始化。

  • int及其子类型(如 int64),默认值为0
  • float32float64 默认值为 +0.000000e+000
  • bool默认值为false
  • string默认值为空值
1
2
3
4
5
6
var defaultInt int
var defaultFloat32 float32
var defaultFloat64 float64
var defaultBool bool
var defaultString string
fmt.Println(defaultInt, defaultFloat32, defaultFloat64, defaultBool, defaultString)

类型转换

在 Go 中,提供了显式强制转换。

1
2
3
var integer16 int16 = 127
var integer32 int32 = 32767
fmt.Println(int32(integer16)+integer32)
1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"strconv"
)

func main() {
i, _ := strconv.Atoi("-42")
s := strconv.Itoa(-42)
fmt.Println(i,s)
}

聚合类型

数组

Go 中的数组是一种特定类型长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。

声明数组

要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
var a [3]int
a[1] = 10
fmt.Println(a[0])
fmt.Println(a[1])
fmt.Println(a[len(a)-1])
}

默认情况下,Go 会用默认数据类型初始化每个元素。len 函数是 Go 中的内置函数,用于获取数组、切片或映射中的元素数。

初始化数组

声明数组时,还可使用非默认值来初始化数组。

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
)

func main() {
cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
fmt.Println("Cities:", cities)
}
1
Cities: [New York Paris Berlin Madrid ]

即使数组应具有 5 个元素,也无需为所有元素分配值。 如上所示,最新位置包含一个空的字符串,因为它是字符串数据类型的默认值。

数组中的省略号

如果你不知道你将需要多少个位置,但知道你将具有多少数据,那么还有一种声明和初始化数组的方法是使用省略号 ...

1
q := [...]int{1, 2, 3}
1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
cities := [...]string{"New York", "Paris", "Berlin", "Madrid"}
fmt.Println("Cities:", cities)
}
1
Cities: [New York Paris Berlin Madrid]

末尾没有空字符串。 数组长度由你初始化它时输入的字符串决定。 如果你不再需要,则不保留你不知道的内存。
另一种有趣的数组初始化方法是使用省略号并仅为最新的位置指定值。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
numbers := [...]int{99: -1}
fmt.Println("First Position:", numbers[0])
fmt.Println("Last Position:", numbers[99])
fmt.Println("Length:", len(numbers))
}
1
2
3
First Position: 0
Last Position: -1
Length: 100

请注意数组的长度是 100,因为你为第 99 个位置指定了一个值。 第一个位置打印出默认值(零)。

多维数组

Go 支持多维数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var twoD [3][5]int
for i := 0; i < 3; i++ {
for j := 0; j < 5; j++ {
twoD[i][j] = (i + 1) * (j + 1)
}
fmt.Println("Row", i, twoD[i])
}
fmt.Println("\nAll at once:", twoD)
}
1
2
3
4
5
Row 0 [1 2 3 4 5]
Row 1 [2 4 6 8 10]
Row 2 [3 6 9 12 15]

All at once: [[1 2 3 4 5] [2 4 6 8 10] [3 6 9 12 15]]

结构

有时,你需要在一个结构中表示字段的集合。 例如,要编写工资核算程序时,需要使用员工数据结构。 在 Go 中,可使用结构将可能构成记录的不同字段组合在一起。
Go 中的结构也是一种数据结构,它可包含零个或多个任意类型的字段,并将它们表示为单个实体。

声明和初始化结构

若要声明结构,需要使用 struct 关键字

1
2
3
4
5
6
type Employee struct {
ID int
FirstName string
LastName string
Address string
}

然后就可以像操作其他类型一样使用新类型声明一个变量

1
var john Employee
1
employee := Employee{1001, "John", "Doe", "Doe's Street"}

请注意,必须为结构中的每个字段指定一个值。 但这有时也可能会导致出现问题。 或者,可更具体地了解要在结构中初始化的字段:

1
employee := Employee{LastName: "Doe", FirstName: "John"}

请注意从上述声明中看,为每个字段分配值的顺序不重要。 此外,如果未指定任何其他字段的值,也并不重要。 Go 将根据字段数据类型分配默认值。
若要访问结构的各个字段,可使用点表示法 (.) 做到这一点,如下例所示:

1
2
employee.ID = 1001
fmt.Println(employee.FirstName)

最后,可使用 & 运算符生成指向结构的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Employee struct {
ID int
FirstName string
LastName string
Address string
}

func main() {
employee := Employee{LastName: "Doe", FirstName: "John"}
fmt.Println(employee)
employeeCopy := &employee
employeeCopy.FirstName = "David"
fmt.Println(employee)
}
1
2
{0 John Doe }
{0 David Doe }

请注意在使用指针时结构是如何变为可变结构的

结构嵌入

通过 Go 中的结构,可将某结构嵌入到另一结构中。 有时,你需要减少重复并重用一种常见的结构。 例如,假设你想要重构之前的代码,使其具有两种数据类型,一种针对员工,一种针对合同工。 你可具有一个包含公共字段的 Person 结构

1
2
3
4
5
6
type Person struct {
ID int
FirstName string
LastName string
Address string
}

然后,你可声明嵌入 Person 类型的其他类型,例如 EmployeeContractor。 若要嵌入另一个结构,请创建一个新字段,如下例所示:

1
2
3
4
type Employee struct {
Information Person
ManagerID int
}

若要引用 Person 结构中的字段,你需要包含员工变量中的 Information 字段

1
2
var employee Employee
employee.Information.FirstName = "John"

如果你要像这样重构代码,则会破坏代码。 或者,你可只包含一个与你要嵌入的结构同名的新字段,如下例所示:

1
2
3
4
type Employee struct {
Person
ManagerID int
}
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
package main

import "fmt"

type Person struct {
ID int
FirstName string
LastName string
Address string
}

type Employee struct {
Person
ManagerID int
}

type Contractor struct {
Person
CompanyID int
}

func main() {
employee := Employee{
Person: Person{
FirstName: "John",
},
}
employee.LastName = "Doe"
fmt.Println(employee.FirstName)
}

请注意如何在无需指定 Person 字段的情况下访问 Employee 结构中的 FirstName 字段,因为它会自动嵌入其所有字段。 但在你初始化结构时,必须明确要给哪个字段分配值。

用 JSON 编码和解码结构

可使用结构来对 JSON 中的数据进行编码和解码。 Go 对 JSON 格式提供很好的支持,该格式已包含在标准库包中。
你还可执行一些操作,例如重命名结构中字段的名称。 例如,假设你不希望 JSON 输出显示 FirstName 而只显示 name,或者忽略空字段, 可使用如下例所示的字段标记:

1
2
3
4
5
6
type Person struct {
ID int
FirstName string `json:"name"`
LastName string
Address string `json:"address,omitempty"`
}

若要将结构编码为 JSON,请使用 json.Marshal 函数。 若要将 JSON 字符串解码为数据结构,请使用 json.Unmarshal 函数。 下例将所有内容组合在一起,将员工数组编码为 JSON,并将输出解码为新的变量:

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
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"encoding/json"
"fmt"
)

type Person struct {
ID int
FirstName string `json:"name"`
LastName string
Address string `json:"address,omitempty"`
}

type Employee struct {
Person
ManagerID int
}

type Contractor struct {
Person
CompanyID int
}

func main() {
employees := []Employee{
Employee{
Person: Person{
LastName: "Doe", FirstName: "John",
},
},
Employee{
Person: Person{
LastName: "Campbell", FirstName: "David",
},
},
}

data, _ := json.Marshal(employees)
fmt.Printf("%s\n", data)

var decoded []Employee
json.Unmarshal(data, &decoded)
fmt.Printf("%v", decoded)
}
1
2
[{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}]
[{{0 John Doe } 0} {{0 David Campbell } 0}]

引用类型

指针

函数

main 函数

程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。
在深入了解如何创建自定义函数的基本知识之前,让我们看看 main() 函数的一个重要特性。 你可能留意到,main() 函数没有任何参数,并且不返回任何内容。 但这并不意味着其不能从用户读取值,如命令行参数。 如要访问 Go 中的命令行参数,可以使用用于保存传递到程序的所有参数的 os 包 和 os.Args 变量来执行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"os"
"strconv"
)

func main(){
number1, _ := strconv.Atoi(os.Args[1])
number2, _ := strconv.Atoi(os.Args[2])
fmt.Println("Sum:", number1+number2)
}

os.Args 变量包含传递给程序的每个命令行参数。 由于这些值的类型为 string,因此需要将它们转换为 int 以进行求和。
若要运行程序,请使用以下命令:

1
go run main.go 3 5

自定义函数

下面是用于创建函数的语法

1
2
3
func name(parameters) (results) {
body-content
}

使用 func 关键字来定义函数,然后为其指定名称。 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。 你还可以定义函数的返回类型,该函数也可以是零个或多个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"os"
"strconv"
)

func main() {
sum := sum(os.Args[1], os.Args[2])
fmt.Println("Sum:", sum)
}

func sum(number1 string, number2 string) int {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
return int1 + int2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"strconv"
)

func main() {
v := "10"
if s, err := strconv.Atoi(v); err == nil {
fmt.Printf("%T, %v", s, s)
}
}

在 Go 中,你还可以为函数的返回值设置名称,将其当作一个变量。

1
2
3
4
5
6
func sum(number1 string, number2 string) (result int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
result = int1 + int2
return
}

请注意,现在需要将函数的结果值括在括号中。 你还可以在函数中使用该变量,并且只需在末尾添加 return 行。 Go 将返回这些返回变量的当前值。 在函数末尾编写 return 关键字非常简单方便,尤其是在有多个返回值时。 我们不建议使用此方法。 可能不确定函数将返回什么。

返回多个值

在 Go 中,函数可以返回多个值。 你可以采用类似于定义函数参数的方式来定义这些值。 换句话说,你可以指定一个类型和名称,但该名称是可选的。

1
2
3
4
5
6
7
func calc(number1 string, number2 string) (sum int, mul int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
sum = int1 + int2
mul = int1 * int2
return
}

更改函数参数值(指针)

Go 是“按值传递”编程语言。和 Java 一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
firstName = "John"
updateName(&fristName)
fmt.Println(fristName)
}

func updateName(name *string) {
*name = "David"
}

& 是取地址符号,用于取得一个变量的地址
在上述代码中,*string表明需要接收指针,含义:指向字符串的指针,随后使用*name将变量name的值暂停,随后传入地址,此时,指针name指向变量firsName的地址,如果name改变,fristname也将改变。

切片

数组是切片和映射的基础。与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。
切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。
切片只有 3 个组件:

  • 指向基础数组中第一个可访问元素的指针。 此元素不一定是数组的第一个元素 array[0]。
  • 切片的长度。 切片中的元素数目。
  • 切片的容量。 切片开头与基础数组结束之间的元素数目。

下图显示了什么是切片:
image.png
请注意,切片只是基础数组的一个子集。

声明和初始化切片

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
fmt.Println(months)
fmt.Println("Length:", len(months))
fmt.Println("Capacity:", cap(months))
}
1
2
3
[January February March April May June July August September October November December]
Length: 12
Capacity: 12

请注意目前,切片与数组的区别不大。 可用相同的方式声明这两者。 若要从切片中获取信息,可使用内置函数 len() 和 cap()。 我们将继续使用这些函数来确认切片可具有来自基础数组的后续元素。

切片项

Go 支持切片运算符 s[i:p],其中:
s 表示数组。
i 表示指向要添加到新切片的基础数组(或另一个切片)的第一个元素的指针。 变量 i 对应于数组 array[i]索引位置**i** 处的元素。 请记住,此元素不一定是基础数组的第一个元素 array[0]
p 表示创建新切片时要使用的基础数组中的元素数目。 变量 p 对应于可用于新切片的基础数组中的最后一个元素。 可在位置 array[i+1] 找到基础数组中位置 p 处的元素。 请注意,此元素不一定是基础数组的最后一个元素 array[len(array)-1]
如你所见,切片只能引用元素的子集
假设你需要 4 个变量来表示一年的每个季度,并且你有一个包含 12 个元素的 months 切片。 下图演示了如何将 months 切片为 4 个新的 quarter 切片:
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter1 := months[0:3]
quarter2 := months[3:6]
quarter3 := months[6:9]
quarter4 := months[9:12]
fmt.Println(quarter1, len(quarter1), cap(quarter1))
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter3, len(quarter3), cap(quarter3))
fmt.Println(quarter4, len(quarter4), cap(quarter4))
}
1
2
3
4
[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

切片的长度不变,但容量不同。 我们来了解 quarter2 切片。 声明此切片时,你指出希望切片从位置编号 3 开始,最后一个元素位于位置编号 6。 切片长度为 3 个元素,但容量为 9,原因是基础数组有更多元素或位置可供使用,但对切片而言不可见。 例如,如果你尝试打印类似 fmt.Println(quarter2[3]) 的内容,会出现以下错误:panic: runtime error: index out of range [3] with length 3
切片容量仅指切片可扩展的程度。 因此,你可从 quarter2 创建扩展切片,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter2 := months[3:6]
quarter2Extended := quarter2[:4]
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}
1
2
[April May June] 3 9
[April May June July] 4 9

请注意在声明 quarter2Extended 变量时,无需指定初始位置 ([:4])。 执行此操作时,Go 会假定你想要切片的第一个位置。 你可对最后一个位置 ([1:]) 执行相同的操作。 Go 将假定你要引用所有元素,直到切片的最新位置 (len()-1)

追加项

Go 提供了内置函数 append(slice, element),便于你向切片添加元素。 将要修改的切片和要追加的元素作为值发送给该函数。 然后,append 函数会返回一个新的切片,将其存储在变量中。 对于要更改的切片,变量可能相同。

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
var numbers []int
for i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
}
}
1
2
3
4
5
6
7
8
9
10
0       cap=1   [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]

此输出很有意思。 特别是对于调用 cap() 函数所返回的内容。 一切看起来都很正常,直到第 3 次迭代,此时容量变为 4,切片中只有 3 个元素。 在第 5 次迭代中,容量又变为 8,第 9 次迭代时变为 16。
注意到容量输出中的模式了吗? 当切片容量不足以容纳更多元素时,Go 的容量将翻倍。 它将新建一个具有新容量的基础数组。 无需执行任何操作即可使容量增加。 Go 会自动扩充容量。 需要谨慎操作。 有时,一个切片具有的容量可能比它需要的多得多,这样你将会浪费内存。

删除项

Go 没有内置函数用于从切片中删除元素。 可使用上述切片运算符 s[i:p] 来新建一个仅包含所需元素的切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
letters := []string{"A", "B", "C", "D", "E"}
remove := 2

if remove < len(letters) {

fmt.Println("Before", letters, "Remove ", letters[remove])

letters = append(letters[:remove], letters[remove+1:]...)

fmt.Println("After", letters)
}

}
1
2
Before [A B C D E]
After [A B D E]

此代码会从切片中删除元素。 它用切片中的下一个元素替换要删除的元素,如果删除的是最后一个元素,则不替换。
另一种方法是创建切片的新副本。

创建切片的副本[深浅拷贝]

Go 具有内置函数 copy(dst, src []Type) 用于创建切片的副本。 你需要发送目标切片和源切片。

1
2
slice2 := make([]string, 3)
copy(slice2, letters[1:4)

为何要创建副本? 更改切片中的元素时,基础数组将随之更改。 引用该基础数组的任何其他切片都会受到影响。 让我们在代码中看看此过程,然后创建一个切片副本来解决此问题。
使用下述代码确认切片指向数组,而你在切片中所做的每个更改都会影响基础数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)

slice1 := letters[0:2]
slice2 := letters[1:4]

slice1[1] = "Z"

fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
1
2
3
Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

请注意对 slice1 所做的更改如何影响 letters 数组和 slice2。 可在输出中看到字母 B 已替换为 Z,它会影响指向 letters 数组的每个切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)

slice1 := letters[0:2]
slice2 := make([]string, 3)
copy(slice2, letters[1:4])

slice1[1] = "Z"

fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
1
2
3
Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

请注意 slice1 中的更改如何影响基础数组,但它并未影响新的 slice2

映射 Map

大体上来说,Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。

声明和初始化映射

若要声明映射,需要使用 map 关键字。 然后,定义键和值类型,如下所示:map[T]T

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
studentAge := map[string]int{
"john": 32,
"bob": 32,
}
fmt.Println(studentAge)
}
1
map[bob:31 john:32]

如果不想使用项来初始化映射,可使用内置函数 make() 在上一部分创建切片。 可使用以下代码创建空映射:

1
studentsAge := make(map[string]int)

映射是动态的。 创建项后,可添加、访问或删除这些项。 让我们来了解这些操作。

添加项

要添加项,无需像对切片一样使用内置函数。 映射更加简单。 你只需定义键和值即可。 如果没有键值对,则该项会添加到映射中。
让我们使用 make 函数重写之前用于创建映射的代码。 然后,将项添加到映射中。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
1
map[bob:31 john:32]

请注意,我们已向已初始化的映射添加了项。 但如果尝试使用 nil 映射执行相同操作,会出现错误。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var studentsAge map[string]int
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
1
2
3
4
5
6
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
/Users/johndoe/go/src/helloworld/main.go:7 +0x4f
exit status 2

若要避免在将项添加到映射时出现问题,请确保使用 make 函数(如我们在上述代码片段中所示)创建一个空映射(而不是 nil 映射)。 此规则仅适用于添加项的情况。 如果在 nil 映射中运行查找、删除或循环操作,Go 不会执行 panic。

访问项

若要访问映射中的项,可使用常用的下标表示法 m[key],就像操作数组或切片一样

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println("Bob's age is", studentsAge["bob"])
}

在映射中使用下标表示法时,即使映射中没有键,你也总会获得响应。 当你访问不存在的项时,Go 不会执行 panic。 此时,你会获得默认值。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println("Christy's age is", studentsAge["christy"])
}
1
Christy's age is 0

在很多情况下,访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31

age, exist := studentsAge["christy"]
if exist {
fmt.Println("Christy's age is", age)
} else {
fmt.Println("Christy's age couldn't be found")
}
}
1
Christy's age couldn't be found

使用第二个代码片段检查映射中的键在你访问之前是否存在。

删除项

若要从映射中删除项,请使用内置函数 delete()

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
delete(studentsAge, "john")
fmt.Println(studentsAge)
}
1
map[bob:31]

映射中的循环

最后,让我们看看如何在映射中进行循环来以编程方式访问其所有的项。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
studentsAge := make(map[string]int)
studentsAge["John"] = 32
studentsAge["bob"] = 31
for name, age := range studentsAge {
fmt.Printf("%s\t%d\n", name, age)
}
}
1
2
john    32
bob 31

请注意可如何将键和值信息存储在不同的变量中。 在本例中,我们将键保存在 name 变量中,将值保存在 age 变量中。 因此,range 会首先生成项的键,然后再生成该项的值。 可使用 _ 变量忽略其中任何一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31

for _, age := range studentsAge {
fmt.Printf("Ages %d\n", age)
}
}

即使在本例中用这种方式打印年龄没有意义,但存在你无需知道项的键的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31

for name := range studentsAge {
fmt.Printf("Names %s\n", name)
}
}

sync.map

map在Go语言并发编程中,如果仅用于读取数据时候是安全的,但是在读写操作的时候是不安全的,在Go语言1.9版本后提供了一种并发安全的,sync.Map是Go语言提供的内置map,不同于基本的map数据类型,所以不能像操作基本map那样的方式操作数据,他提供了特有的方法,不需要初始化操作实现增删改查的操作。

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
package main

import (
"fmt"
"sync"
)

//声明sync.Map
var syncmap sync.Map

func main() {

//Store方法将键值对保存到sync.Map
syncmap.Store("zhangsan", 97)
syncmap.Store("lisi", 100)
syncmap.Store("wangmazi", 200)

// Load方法获取sync.Map 键所对应的值
fmt.Println(syncmap.Load("lisi"))

// Delete方法键删除对应的键值对
syncmap.Delete("lisi")

// Range遍历所有sync.Map中的键值对
syncmap.Range(func(k, v interface{}) bool {
fmt.Println(k, v)
return true
})

}

通道

接口类型

main包

通常情况下,默认包是 main 包。
换当使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(具有 .a 扩展名的文件)。
在 Go 中,包名称需遵循约定。 包使用其导入路径的最后一部分作为名称。 例如,Go 标准库包含名为 math/cmplx 的包,该包提供用于处理复数的有用代码。 此包的导入路径为 math/cmplx,导入包的方式如下所示:

1
import "math/cmplx"

若要引用包中的对象,请使用包名称 cmplx,如下所示:

1
cmplx.Inf()

创建包

案例:在 $GOPATH/src下创建目录calculator和名为 sum.go 的文件,然后初始化此文件。

1
package calculator

现在可以开始编写包的函数和变量。
不同于其他编程语言,Go 不会提供 publicprivate 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:

  • 如需将某些内容设为专用内容,请以小写字母开始。
  • 如需将某些内容设为公共内容,请以大写字母开始。

补全代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package calculator

var logMessage = "[LOG]"

var Version = "1.0"

func internalSum(number int) int {
return number-1
}

func Sum(number1, number2 int) int {
return number1 + number2
}

让我们看一下该代码中的一些事项:

  • 只能从包内调用 logMessage 变量。
  • 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
  • 只能从包内调用 internalSum 函数。
  • 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。

若要确认一切正常,可在 calculator 目录中运行 go build 命令。 如果执行此操作,请注意系统不会生成可执行的二进制文件。

创建模块

Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文环境。 此上下文信息包括编写代码时所用的 Go 版本。
此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。
若要为 calculator 包创建模块,请在根目录 ($GOPATH/src/calculator) 中运行以下命令

1
go mod init github.com/myuser/calculator

运行此命令后,github.com/myuser/calculator 就会变成模块的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 go.mod 的新文件。 最后,树目录现会如下列目录所示:

1
2
3
4
src/
calculator/
go.mod
sum.go

go.mod 内容应该如下代码所示: (Go 版本可能不同。)

1
2
3
module github.com/myuser/calculator

go 1.20

引用本地包(模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"

"github.com/myuser/calculator"
)

func main() {
total := calculator.Sum(3, 5)
fmt.Println(total)
fmt.Println("Version: ", calculator.Version)
}

如果立即尝试运行程序,它将不起任何作用。 你需要告诉 Go,你会使用模块来引用其他包。 为此,请在 $GOPATH/src/helloworld 目录中运行以下命令:

1
go mod init helloworld

此举会创建一个新的 mod文件,文件目录如下:

1
2
3
4
5
6
7
src/
calculator/
go.mod
sum.go
helloworld/
go.mod
main.go

由于你引用的是该模块的本地副本,因此你需要通知 Go 不要使用远程位置。 因此,你需要手动修改 go.mod文件,使其包含引用,如下所示:

1
2
3
4
5
6
7
module helloworld

go 1.14

require github.com/myuser/calculator v0.0.0

replace github.com/myuser/calculator => ../calculator

replace 关键字指定使用本地目录,而不是模块的远程位置。 在这种情况下,由于 helloworldcalculator 程序在 $GOPATH/src 中,因此位置只能是 ../calculator。 如果模块源位于不同的位置,请在此处定义本地路径。

发布包

如果想要将 calculator 包发布到 GitHub 帐户,则需要创建一个名为 calculator 的存储库。 URL 应与下述网址类似

1
https://github.com/myuser/calculator

将通过标记存储库来对包进行版本化,如下所示:

1
2
git tag v0.1.0
git push origin v0.1.0
1
import "github.com/myuser/calculator"

引用外部(第三方)包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"github.com/myuser/calculator"
"rsc.io/quote"
)

func main() {
total := calculator.Sum(3, 5)
fmt.Println(total)
fmt.Println("Version: ", calculator.Version)
fmt.Println(quote.Hello())
}

保存后需要更新 go mod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module helloworld

go 1.20

require (
github.com/myuser/calculator v0.0.0
rsc.io/quote v1.5.2
)

require (
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
rsc.io/sampler v1.3.0 // indirect
)

replace github.com/myuser/calculator => ../calculator

控制流

if/else 条件表达式

与其他编程语言不同的是,在 Go 中,你不需要在条件中使用括号。 else 子句可选。 但是,大括号仍然是必需的。 此外,为了减少行,Go 不支持三元 if 语句,因此每次都需要编写完整的 if 语句。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
x := 27
if x%2 == 0 {
fmt.Println(x, "is even")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func givemeanumber() int {
return -1
}

func main() {
if num := givemeanumber(); num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has only one digit")
} else {
fmt.Println(num, "has multiple digits")
}
}

请注意,在此代码中,num 变量存储从 givemeanumber() 函数返回的值,并且该变量在所有 if分支中可用。

switch 控制器

像其他编程语言一样,Go 支持 switch 语句。 可以使用 switch 语句来避免链接多个 if 语句。 使用 switch 语句,就不需维护和读取包含多个 if 语句的代码。 这些语句还可以让复杂的条件更易于构造。

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 main

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

func main() {
sec := time.Now().Unix()
rand.Seed(sec)
i := rand.Int31n(10)
fmt.Println("i is :", i)
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
}
fmt.Println("ok")
}

多表达式匹配

如果希望 case 语句包含多个表达式,请使用逗号 , 来分隔表达式。 此方法可避免代码重复。

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

import "fmt"

func location(city string) (string, string) {
var region string
var continent string
switch city {
case "Delhi", "Hyderabad", "Mumbai", "Chennai", "Kochi":
region, continent = "India", "Asia"
case "Lafayette", "Louisville", "Boulder":
region, continent = "Colorado", "USA"
case "Irvine", "Los Angeles", "San Diego":
region, continent = "California", "USA"
default:
region, continent = "Unknown", "Unknown"
}
return region, continent
}
func main() {
region, continent := location("Irvine")
fmt.Printf("John works in %s, %s\n", region, continent)
}

调用函数

switch 还可以调用函数。 在该函数中,可以针对可能的返回值编写 case 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func main() {
switch time.Now().Weekday().String() {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("It's time to learn some Go.")
default:
fmt.Println("It's the weekend, time to rest!")
}

fmt.Println(time.Now().Weekday().String())
}

此外,还可以从 case 语句调用函数。

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

import (
"fmt"
"regexp"
)

func main() {
var email = regexp.MustCompile(`^[^@]+@[^@.]+\.[^@.]+`)
var phone = regexp.MustCompile(`^[(]?[0-9][0-9][0-9][). \-]*[0-9][0-9][0-9][.\-]?[0-9][0-9][0-9][0-9]`)

contact := "foo@bar.com"

switch {
case email.MatchString(contact):
fmt.Println(contact, "is an email")
case phone.MatchString(contact):
fmt.Println(contact, "is a phone number")
default:
fmt.Println(contact, "is not recognized")
}
}

省略条件

在 Go 中,可以在 switch语句中省略条件,就像在 if 语句中那样。 此模式类似于 true 值,就像强制 switch 语句一直运行一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

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

func main() {
rand.Seed(time.Now().Unix())
r := rand.Float64()
switch {
case r > 0.1:
fmt.Println("Common case, 90% of the time")
default:
fmt.Println("10% of the time")
}
}

使逻辑进入到下一个 case

在某些编程语言中,你会在每个 case 语句末尾写一个 break 关键字。 但在 Go 中,当逻辑进入某个 case 时,它会退出 switch 块,除非你显式停止它。 若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main() {
switch num := 15; {
case num < 50:
fmt.Printf("%d is less than 50\n", num)
fallthrough
case num > 100:
fmt.Printf("%d is greater than 100\n", num)
fallthrough
case num < 200:
fmt.Printf("%d is less than 200", num)
}
}

请注意,由于 num 为 15(小于 50),因此它与第一个 case 匹配。 但是,num 不大于 100。 由于第一个 case 语句包含 fallthrough 关键字,因此逻辑会立即转到下一个 case 语句,而不会对该 case 进行验证。 因此,在使用 fallthrough 关键字时必须谨慎。 该代码产生的行为可能不是你想要的。

for 循环表达式

Go 只使用一个循环构造,即 for 循环。与 if 语句和 switch 语句一样,for 循环表达式不需要括号。 但是,大括号是必需的。
分号 ; 分隔 for 循环的三个组件:

  • 在第一次迭代之前执行的初始语句(可选)。
  • 在每次迭代之前计算的条件表达式。 该条件为 false 时,循环会停止。
  • 在每次迭代结束时执行的后处理语句(可选)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum of 1..100 is", sum)
}

空的预处理语句和后处理语句

只要 num 变量保存的值与 5 不同,程序就会输出一个随机数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

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

func main() {
var num int64
rand.Seed(time.Now().Unix())
for num != 5 {
num = rand.Int63n(15)
fmt.Println(num)
}
}

无限循环和 break 语句

在这种情况下,你不编写条件表达式,也不编写预处理语句或后处理语句, 而是采取退出循环的方式进行编写。 否则,逻辑永远都不会退出。 若要使逻辑退出循环,请使用 break 关键字。

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

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

func main() {
var num int32
sec := time.Now().Unix()
rand.Seed(sec)

for {
fmt.Print("Writing inside the loop...")
if num = rand.Int31n(10); num == 5 {
fmt.Println("finish!")
break
}
fmt.Println(num)
}
}

Continue 语句

在 Go 中,可以使用 continue 关键字跳过循环的当前迭代。 例如,可以使用此关键字在循环继续之前运行验证。 也可以在编写无限循环并需要等待资源变得可用时使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
sum := 0
for num := 1; num <= 100; num++ {
if num%5 == 0 {
continue
}
sum += num
}
fmt.Println("The sum of 1 to 100, but excluding numbers divisible by 5, is", sum)
}

defer/panic/recover函数

Go 特有的一些控制流:deferpanicrecover

defer 函数

在 Go 中,defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成。 通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。
可以根据需要推迟任意多个函数。 defer 语句按逆序运行,先运行最后一个,最后运行第一个。
有点像

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
for i := 1; i <= 4; i++ {
defer fmt.Println("deferred", -i)
fmt.Println("regular", i)
}
}
1
2
3
4
5
6
7
8
regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

defer 函数的一个典型用例是在使用完文件后将其关闭。

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

import (
"io"
"os"
"fmt"
)

func main() {
newfile, error := os.Create("learnGo.txt")
if error != nil {
fmt.Println("Error: Could not create file.")
return
}
defer newfile.Close()

if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
fmt.Println("Error: Could not write to file.")
return
}

newfile.Sync()
}

创建或打开某个文件后,可以推迟 .Close() 函数的执行,以免在你完成后忘记关闭该文件。

panic 函数

运行时错误会使 Go 程序崩溃,例如尝试通过使用超出范围的索引或取消引用 nil 指针来访问数组。 你也可以强制程序崩溃。
内置 panic() 函数可以停止 Go 程序中的正常控制流。 当你使用 panic 调用时,任何延迟的函数调用都将正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误信息和堆栈跟踪,有助于诊断问题的根本原因。
调用 panic() 函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。
例如,下面的代码将 panicdefer 函数组合在一起。 尝试运行此代码以了解控制流的中断。 请注意,清理过程仍会运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func highlow(high int, low int) {
if high < low {
fmt.Println("Panic!")
panic("highlow() low greater than high")
}
defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
fmt.Println("Call: highlow(", high, ",", low, ")")

highlow(high, low + 1)
}

func main() {
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
/tmp/sandbox/prog.go:18 +0x298
main.main()
/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

下面是运行代码时会发生的情况:

  1. 一切正常运行。 程序将输出传递到 highlow() 函数中的高值和低值。
  2. 如果 low 的值大于 high 的值,则程序会崩溃。 会显示“Panic!”消息。 此时,控制流中断,所有推迟的函数都开始输出“Deferred...”消息。
  3. 程序崩溃,并显示完整的堆栈跟踪。 不会显示“Program finished successfully!”消息。

在发生未预料到的严重错误时,系统通常会运行对 panic() 函数的调用。 若要避免程序崩溃,可以使用名为 recover() 的另一个函数。

recover 函数【类异常处理】

有时,你可能想要避免程序崩溃,改为在内部报告错误。 或者,你可能想要先清理混乱情况,然后再让程序崩溃。 例如,你可能想要关闭与某个资源的连接,以免出现更多问题。
Go 提供内置 recover() 函数,让你可以在程序崩溃之后重新获得控制权。 只会在你同时调用 defer 的函数中调用 recover。 如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer func() {
handler := recover()
if handler != nil {
fmt.Println("main(): recover", handler)
}
}()

highlow(2, 0)
fmt.Println("Program finished successfully!")
}
1
2
3
4
5
6
7
8
9
10
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

panicrecover 函数的组合是 Go 处理异常的惯用方式。 其他编程语言使用 try/catch 块。 Go 首选此处所述的方法。Go 不支持异常。

错误处理和日志记录

有时,你所编写的程序的行为不符合预期。 有时存在一些你无法控制的外部因素,例如其他进程阻止了文件,或者尝试访问不再可用的内存地址。 失败只是程序可能具有的另一种行为。 如果能预见这些失败,就能在它们出现时解决问题。
正如你所了解的那样,Go 的异常处理方法与其他语言不同,其错误处理过程也是如此。 在 Go 中,可能失败的函数应始终返回一个额外值,以便你能够成功预测和管理失败。 例如,你可以运行默认行为并记录尽可能多的信息以再现并修复问题。

错误处理

Go 的错误处理方法只是一种只需要 ifreturn 语句的控制流机制。 例如,在调用函数以从 employee 对象获取信息时,可能需要了解该员工是否存在。 Go 处理此类预期错误的一贯方法如下所示:

1
2
3
4
employee, err := getInformation(1000)
if err != nil {
// Something is wrong. Do something.
}

注意 getInformation 函数返回了 employee 结构,还返回了错误作为第二个值。 该错误可能为 nil。 如果错误为 nil,则表示成功。 如果错误不是 nil,则表示失败。 非 nil 错误附带一条错误消息,你可以打印该错误消息,也可以记录该消息(更可取)。 这是在 Go 中处理错误的方式。 下一部分将介绍一些其他策略。
你可能会注意到,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
package main

import (
"fmt"
"os"
)

type Employee struct {
ID int
FirstName string
LastName string
Address string
}

func main() {
employee, err := getInformation(1001)
if err != nil {
// Something is wrong. Do something.
} else {
fmt.Print(employee)
}
}

func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}

从现在开始,我们将重点介绍如何修改 getInformationapiCallEmployeemain 函数,以展示如何处理错误。

错误处理策略

当函数返回错误时,该错误通常是最后一个返回值。 正如上一部分所介绍的那样,调用方负责检查是否存在错误并处理错误。 因此,一个常见策略是继续使用该模式在子例程中传播错误。 例如,子例程(如上一示例中的 getInformation)可能会将错误返回给调用方,而不执行其他任何操作。

1
2
3
4
5
6
7
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, err // Simply return the error to the caller.
}
return employee, nil
}

你可能还需要在传播错误之前添加更多信息。 为此,可以使用 fmt.Errorf() 函数,该函数与我们之前看到的函数类似,但它返回一个错误。 例如,你可以向错误添加更多上下文,但仍返回原始错误.

1
2
3
4
5
6
7
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
}
return employee, nil
}

另一种策略是在错误为暂时性错误时运行重试逻辑。 例如,可以使用重试策略调用函数三次并等待两秒钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
func getInformation(id int) (*Employee, error) {
for tries := 0; tries < 3; tries++ {
employee, err := apiCallEmployee(1000)
if err == nil {
return employee, nil
}

fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
}

return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

最后,可以记录错误并对最终用户隐藏任何实现详细信息,而不是将错误打印到控制台。 我们将在下一模块介绍日志记录。 现在,让我们看看如何创建和使用自定义错误。

创建可重用的错误

有时错误消息数会增加,你需要维持秩序。 或者,你可能需要为要重用的常见错误消息创建一个库。 在 Go 中,你可以使用 errors.New() 函数创建错误并在若干部分中重复使用这些错误。

1
2
3
4
5
6
7
8
9
var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
if id != 1001 {
return nil, ErrNotFound
}
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}

getInformation 函数的代码外观更优美,而且如果需要更改错误消息,只需在一个位置更改即可。 另请注意,惯例是为错误变量添加 Err 前缀。
最后,如果你具有错误变量,则在处理调用方函数中的错误时可以更具体。 errors.Is() 函数允许你比较获得的错误的类型

1
2
3
4
5
6
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
fmt.Printf("NOT FOUND: %v\n", err)
} else {
fmt.Print(employee)
}

用于错误处理的推荐做法

在 Go 中处理错误时,请记住下面一些推荐做法:

  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • 创建尽可能多的可重用错误变量。
  • 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。
  • 在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。

日志记录

日志在程序中发挥着重要作用,因为它们是在出现问题时你可以检查的信息源。 通常,发生错误时,最终用户只会看到一条消息,指示程序出现问题。 从开发人员的角度来看,我们需要简单错误消息以外的更多信息。 这主要是因为我们想要再现该问题以编写适当的修补程序。

log 包

对于初学者,Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt 包一样使用此包。 该标准包不提供日志级别,且不允许为每个包配置单独的记录器。 如果需要编写更复杂的日志记录配置,可以使用记录框架执行此操作。

1
2
3
4
5
6
7
import (
"log"
)

func main() {
log.Print("Hey, I'm a log!")
}
1
2023/04/07 13:37:06 Hey, I'm a log!

默认情况下,log.Print() 函数将日期和时间添加为日志消息的前缀。 你可以通过使用 fmt.Print() 获得相同的行为,但使用 log 包还能执行其他操作,例如将日志发送到文件。
可以使用 log.Fatal() 函数记录错误并结束程序,就像使用 os.Exit(1) 一样。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"log"
)

func main() {
log.Fatal("Hey, I'm an error log!")
fmt.Print("Can you see me?")
}
1
2
2023/04/07 13:40:14 Hey, I'm a log!
exit status 1

注意最后一行 fmt.Print("Can you see me?") 未运行。 这是因为 log.Fatal() 函数调用停止了该程序。 在使用 log.Panic() 函数时会出现类似行为,该函数也调用 panic() 函数。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
"log"
)

func main() {
log.Panic("Hey, I'm an error log!")
fmt.Print("Can you see me?")
}
1
2
3
4
5
6
7
8
panic: Hey, I'm an error log!

goroutine 1 [running]:
log.Panic({0xc0000bff50?, 0xc0000bff70?, 0xa65859?})
C:/Program Files/Go/src/log/log.go:384 +0x65
main.main()
E:/Work_HK/Code/GoProjects/src/helloworld/main.go:9 +0x48
exit status 2

你仍获得日志消息,但现在还会获得错误堆栈跟踪。
另一重要函数是 log.SetPrefix()。 可使用它向程序的日志消息添加前缀。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"log"
)

func main() {
log.SetPrefix("main(): ")
log.Print("Hey, I'm a log!")
log.Fatal("Hey, I'm an error log!")
}
1
2
3
main(): 2023/04/07 13:44:42 Hey, I'm a log!
main(): 2023/04/07 13:44:42 Hey, I'm an error log!
exit status 1

只需设置一次前缀,日志就会包含日志源自的函数的名称等信息。
可以https://golang.org/pkg/log/

记录到文件

在文件中添加日志后,可以将所有日志集中在一个位置,并将它们与其他事件关联。 此模式为典型模式:具有可能是临时的分布式应用程序,例如容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"os"
)

func main() {
file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}

defer file.Close()

log.SetOutput(file)
log.Print("Hey, I'm a log!")
}

运行前面的代码时,在控制台中看不到任何内容。 在目录中,你应看到一个名为 info.log 的新文件,其中包含使用 log.Print() 函数发送的日志。 请注意,需要首先创建或打开文件,然后将 log 包配置为将所有输出发送到文件。 然后,可以像通常做法那样继续使用 log.Print() 函数。

记录框架

可能有 log 包中的函数不足以处理问题的情况。 你可能会发现,使用记录框架而不编写自己的库很有用。 Go 的几个记录框架有 LogruszerologzapApex
让我们来了解一下可以用 zerolog 做什么。
首先,你需要安装包。

1
go get -u github.com/rs/zerolog/log

我在安装的时候有报错
于是使用了:go install github.com/rs/zerolog/log@latest

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Print("Hey! I'm a log message!")
}
1
{"level":"debug","time":1609855453,"message":"Hey! I'm a log message!"}

请注意,你只需包含正确的导入名称,然后便可以像通常做法那样继续使用 log.Print() 函数。 另请注意输出更改为 JSON 格式。 在集中位置运行搜索时,JSON 是一种有用的日志格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix

log.Debug().
Int("EmployeeID", 1001).
Msg("Getting employee information")

log.Debug().
Str("Name", "John").
Send()
}
1
2
{"level":"debug","EmployeeID":1001,"time":1609855731,"message":"Getting employee information"}
{"level":"debug","Name":"John","time":1609855731}

注意是如何将员工 ID 添加为上下文。 它作为另一属性成为 logline 的一部分。 另外,务必要强调的是,你包含的字段是强类型的。
你可以使用 zerolog 实现其他功能,例如使用分级的日志记录、使用格式化的堆栈跟踪,以及使用多个记录器实例来管理不同输出。 有关详细信息,请参阅 GitHub 站点

重点总结

如你所见,Go 中的错误处理和日志记录与其他编程语言中的这些过程不同。 首先,Go 的错误处理方法非常简单。 使用 if 条件,调用的函数应返回多个值。 按照惯例,最后一个返回值为错误。 如果错误变量返回 nil,则不存在错误。 如果值不为 nil,则存在失败。 只需再次返回错误即可将错误传播到堆栈,并且可以根据需要添加更多上下文。
可以创建可重用为程序中常见错误消息的返回值的错误变量。
你还需要了解何时使用 panic。 我们已介绍 panicrecover 的工作原理。 仅当明确需要停止程序时,才应使用这些函数。 有时,即使你正确处理了错误,程序也可能会停止响应。 但这应该是异常,而不是规则。
最后,我们探讨了 Go 中日志记录的工作原理,你了解了如何使用标准库。 除了将日志打印到控制台之外,你还可以将日志发送到文件供稍后处理,然后将它们发送到一个集中位置。 当代码库扩大时,你可能需要执行其他操作,例如设置日志级别或配置不同输出。 标准库中不支持这些任务。 你将需要使用记录框架,例如 zerolog。

方法和接口

方法

Go 中的方法是一种特殊类型的函数,但存在一个简单的区别:你必须在函数名称之前加入一个额外的参数。 此额外参数称为“接收方”。
如你希望分组函数并将其绑定到自定义类型,则方法非常有用。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。

声明方法

到目前为止,你仅将结构用作可在 Go 中创建的另一种自定义类型。 在此模块中你将了解到,通过添加方法你可以将行为添加到你所创建的结构中。

1
2
3
func (variable type) MethodName(parameters ...) {
// method functionality
}

但是,在声明方法之前,必须先创建结构。 假设你想要创建一个几何包,并决定创建一个名为 triangle 的三角形结构作为此程序包的一个组成部分。 然后,你需要使用一种方法来计算此三角形的周长。

1
2
3
4
5
6
7
type triangle struct {
size int
}

func (t triangle) perimeter() int {
return t.size * 3
}

结构看起来像普通结构,但 perimeter() 函数在函数名称之前有一个类型 triangle 的额外参数。 此接收方意味着,在使用结构时,你可以按如下方式调用函数:

1
2
3
4
func main() {
t := triangle{3}
fmt.Println("Perimeter:", t.perimeter())
}

如果尝试按平常的方式调用 perimeter() 函数,则此函数将无法正常工作,因为此函数的签名表明它需要接收方。 调用此方法的唯一方式是先声明一个结构,获取此方法的访问权限。 只要此方法属于不同的结构,你甚至可以为其指定相同的名称。 例如,你可以使用 perimeter() 函数声明一个 square 结构,具体如下所示:

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"

type triangle struct {
size int
}

type square struct {
size int
}

func (t triangle) perimeter() int {
return t.size * 3
}

func (s square) perimeter() int {
return s.size * 4
}

func main() {
t := triangle{3}
s := square{4}
fmt.Println("Perimeter (triangle):", t.perimeter())
fmt.Println("Perimeter (square):", s.perimeter())
}
1
2
Perimeter (triangle): 9
Perimeter (square): 16

通过对 perimeter() 函数的两次调用,编译器将根据接收方类型来确定要调用的函数。 此行为有助于在各程序包之间保持函数的一致性和名称的简短,并避免将包名称作为前缀。 在下一个单元讲解接口时,我们将介绍此行为的重要性。

方法中的指针

有时,方法需要更新变量。 或者,如果方法的参数太大,你可能希望避免复制它。 在遇到此类情况时,你需要使用指针传递变量的地址。 在之前的模块中,当我们在讨论指针时提到,每次在 Go 中调用函数时,Go 都会复制每个参数值以便使用。
如果你需要更新方法中的接收方变量,也会执行相同的行为。 例如,假设你要创建一个新方法以使三角形的大小增加一倍。 你需要在接收方变量中使用指针.

1
2
3
func (t *triangle) doubleSize() {
t.size *= 2
}
1
2
3
4
5
6
func main() {
t := triangle{3}
t.doubleSize()
fmt.Println("Size:", t.size)
fmt.Println("Perimeter:", t.perimeter())
}
1
2
Size: 6
Perimeter: 18

如果方法仅可访问接收方的信息,则不需要在接收方变量中使用指针。 但是,依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方。 即使此结构的某个方法不需要它也是如此。

声明其他类型的方法

方法的一个关键方面在于,需要为任何类型定义方法,而不只是针对自定义类型(如结构)进行定义。 但是,你不能通过属于其他包的类型来定义结构。 因此,不能在基本类型(如 string)上创建方法。
尽管如此,你仍然可以利用一点技巧,基于基本类型创建自定义类型,然后将其用作基本类型。 例如,假设你要创建一个方法,以将字符串从小写字母转换为大写字母。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"strings"
)

type upperstring string

func (s upperstring) Upper() string {
return strings.ToUpper(string(s))
}

func main() {
s := upperstring("Learning Go!")
fmt.Println(s)
fmt.Println(s.Upper())
}
1
2
Learning Go!
LEARNING GO!

请注意,你在使用新对象 s 时,可以在首次打印其值时将其作为字符串。 然后,你在调用 Upper 方法时,s 会打印出类型字符串的所有大写字母。

嵌入方法

在之前的模块中,您已了解到可以在一个结构中使用属性,并将同一属性嵌入另一个结构中。 也就是说,可以重用来自一个结构的属性,以避免出现重复并保持代码库的一致性。 类似的观点也适用于方法。 即使接收方不同,也可以调用已嵌入结构的方法。
例如,假设你想要创建一个带有逻辑的新三角形结构,以加入颜色。 此外,你还希望继续使用之前声明的三角形结构。

1
2
3
4
type coloredTriangle struct {
triangle
color string
}

然后,你可以初始化 coloredTriangle 结构,并从 triangle 结构调用 perimeter() 方法(甚至访问其字段)

1
2
3
4
5
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter", t.perimeter())
}
1
2
Size: 3
Perimeter 9

如果你熟悉 Java 或 C++ 等 OOP 语言,则可能会认为 triangle 结构看起来像基类,而 coloredTriangle 是一个子类(如继承),但事实并不是如此。 实际上,Go 编译器会通过创建如下的包装器方法来推广 perimeter() 方法:

1
2
3
func (t coloredTriangle) perimeter() int {
return t.triangle.perimeter()
}

请注意,接收方是 coloredTriangle,它从三角形字段调用 perimeter() 方法。 好的一点在于,你不必再创建之前的方法。 你可以选择创建,但 Go 已在内部为你完成了此工作。

重载方法

让我们回到之前讨论过的 triangle 示例。 如果要在 coloredTriangle 结构中更改 perimeter() 方法的实现,会发生什么情况? 不能存在两个同名的函数。 但是,因为方法需要额外参数(接收方),所以,你可以使用一个同名的方法,只要此方法专门用于要使用的接收方即可。 利用这种区别就是重载方法的方式。
:::info
简而言之,接收方不一样,函数名可以一样,这就是重载。
:::
换而言之,如你想要更改其行为,可以编写我们讨论过的包装器方法。 如果彩色三角形的周长是普通三角形的两倍,则代码将如下所示:

1
2
3
func (t coloredTriangle) perimeter() int {
return t.size * 3 * 2
}

现在,无需更改之前编写的 main() 方法中的任何其他内容,具体将如下所示:

1
2
3
4
5
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter", t.perimeter())
}
1
2
Size: 3
Perimeter 18

但是,如果你仍需要从 triangle 结构调用 perimeter() 方法,则可通过对其进行显示访问来执行此操作,如下所示:

1
2
3
4
5
6
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter (colored)", t.perimeter())
fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}
1
2
3
Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

可能已经注意到,在 Go 中,你可以替代方法,并在需要时仍访问原始方法。

封装方法

“封装”表示对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 privatepublic 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法。
Go 中的封装仅在之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。
如要进行尝试,请创建新包 geometry 并按如下方式将三角形结构移入其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package geometry

type Triangle struct {
size int
}

func (t *Triangle) doubleSize() {
t.size *= 2
}

func (t *Triangle) SetSize(size int) {
t.size = size
}

func (t *Triangle) Perimeter() int {
t.doubleSize()
return t.size * 3
}
1
2
3
4
5
func main() {
t := geometry.Triangle{}
t.SetSize(3)
fmt.Println("Perimeter", t.Perimeter())
}

若访问 doubleSize() 方法,则程序会死机。

1
./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)

匿名方法

在该函数定义时调用,只需在大括号结束后,使用小括号将要传入的参数值包裹起来即可,比如:

1
2
3
4
5
func main() {
func(text string) {
fmt.Prinln(text);
}("hello go")
}

另一种调用匿名函数的方法是将匿名函数赋值给某个变量,然后通过变量调用。

1
2
3
4
5
6
func main(){
happy := func(text string){
ftm.Println(text);
}
happy("hello")
}

接口

Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。 因此,你可以快速扩展程序的功能。
与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。 Go 不提供用于实现接口的关键字。 因此,如果你之前使用的是其他编程语言中的接口,但不熟悉 Go,那么此概念可能会造成混淆。

声明接口

Go 中的接口类似于蓝图。 一种抽象类型,只包括具体类型必须拥有或实现的方法。
假设你希望在几何包中创建一个接口来指示形状必须实现的方法。 你可以按如下所示定义接口:

1
2
3
4
type Shape interface {
Perimeter() float64
Area() float64
}

Shape 接口表示你想要考虑 Shape 的任何类型都需要同时具有 Perimeter()Area() 方法。 例如,在创建 Square 结构时,它必须实现两种方法,而不是仅实现一种。 另外,请注意接口不包含这些方法的实现细节(例如,用于计算某个形状的周长和面积)。 接口仅表示一种协定。 三角形、圆圈和正方形等形状有不同的计算面积和周长方式。

实现接口

正如上文所讨论的内容,你没有用于实现接口的关键字。 当 Go 中的接口具有接口所需的所有方法时,则满足按类型的隐式实现。
让我们创建一个 Square 结构,此结构具有 Shape 接口中的两个方法。

1
2
3
4
5
6
7
8
9
10
11
type Square struct {
size float64
}

func (s Square) Area() float64 {
return s.size * s.size
}

func (s Square) Perimeter() float64 {
return s.size * 4
}

实现字符串接口

扩展现有功能的一个简单示例是使用 Stringer,它是具有 String() 方法的接口,具体如下所示:

1
2
3
type Stringer interface {
String() string
}

fmt.Printf 函数使用此接口来输出值,这意味着你可以编写自定义 String() 方法来打印自定义字符串,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type Person struct {
Name, Country string
}

func (p Person) String() string {
return fmt.Sprintf("%v is from %v", p.Name, p.Country) // 不用 Printf 因为返回值不同
}
func main() {
rs := Person{"John Doe", "USA"}
ab := Person{"Mark Collins", "United Kingdom"}
fmt.Printf("%s\n%s\n", rs, ab)
}
1
2
John Doe is from USA
Mark Collins is from United Kingdom

如你所见,你已使用自定义类型(结构)来写入 String() 方法的自定义版本。 此方法是在 Go 中实现接口的一种常用方法。

扩展所有实现

假设你具有以下代码,并且希望通过编写负责处理某些数据的 Writer 方法的自定义实现来扩展其功能。
通过使用以下代码,你可以创建一个程序,此程序使用 GitHub API 从 Microsoft 获取三个存储库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"io"
"net/http"
"os"
)

func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

io.Copy(os.Stdout, resp.Body)
}
1
2
[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

请注意,io.Copy(os.Stdout, resp.Body) 调用是指将通过对 GitHub API 的调用获取的内容打印到终端。 假设你想要写入自己的实现以缩短你在终端中看到的内容。
在查看 io.Copy 函数的源 时,你将看到:

1
func Copy(dst Writer, src Reader) (written int64, err error)

如果你深入查看第一个参数 dst Writer 的详细信息,你会注意到 Writer 是 接口:

1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

由于 Writer 是接口,并且是 Copy 函数需要的对象,你可以编写 Write 方法的自定义实现。 因此,你可以自定义打印到终端的内容。
实现接口所需的第一项操作是创建自定义类型。 在这种情况下,你可以创建一个空结构,因为你只需按如下所示编写自定义 Write 方法即可:

1
type customWriter struct{}

现在,你已准备就绪,可开始编写自定义 Write 函数。 此时,你还需要编写一个结构,以便将 JSON 格式的 API 响应解析为 Golang 对象。 你可以使用“JSON 转 Go”站点从 JSON 有效负载创建结构。 因此,Write 方法可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
type GitHubResponse []struct {
FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}

最后,你必须修改 main() 函数以使用你的自定义对象,具体如下所示:

1
2
3
4
5
6
7
8
9
10
func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

writer := customWriter{}
io.Copy(writer, resp.Body)
}
1
2
3
4
5
microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template
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
35
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)

type GitHubResponse []struct {
FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
var resp GitHubResponse
json.Unmarshal(p, &resp)
for _, r := range resp {
fmt.Println(r.FullName)
}
return len(p), nil
}

func main() {
resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}

writer := customWriter{}
io.Copy(writer, resp.Body)
}

编写自定义服务器API

如果你要创建服务器 API,你可能会发现此用例非常实用。 编写 Web 服务器的常用方式是使用 net/http 程序包中的 http.Handler 接口,具体如下所示(无需写入此代码)

1
2
3
4
5
6
7
package http

type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

请注意 ListenAndServe 函数需要服务器地址(如 http://localhost:8000)以及将响应从调用调度至服务器地址的 Handler 的实例。

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"
"net/http"
)

type dollars float32

func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}

func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
1
go run main.go

如果没有得到任何输出,说明情况不错。 此时,在新浏览器窗口中打开 http://localhost:8000,或在终端中运行以下命令:

1
curl http://localhost:8000
1
2
Go T-Shirt: $25.00
Go Jacket: $55.00

让我们一起慢慢回顾之前的代码,了解其用途并观察 Go 接口的功能。 首先,创建 float32 类型的自定义类型,然后编写 String() 方法的自定义实现,以便稍后使用。

1
2
3
4
5
type dollars float32

func (d dollars) String() string {
return fmt.Sprintf("$%.2f", d)
}

然后,写入 http.Handler 可使用的 ServeHTTP 方法的实现。 请注意,我们重新创建了自定义类型,但这次它是映射,而不是结构。 接下来,我们通过使用 database 类型作为接收方来写入 ServeHTTP 方法。 此方法的实现使用来自接收方的数据,然后对其进行循环访问,再输出每一项。

1
2
3
4
5
6
7
type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}

最后,在 main() 函数中,我们将 database 类型实例化,并使用一些值对其进行初始化。 我们使用 http.ListenAndServe 函数启动了 HTTP 服务器,在其中定义了服务器地址,包括要使用的端口和实现 ServeHTTP 方法自定义版本的 db 对象。 在你运行程序时,Go 将使用此方法的实现,这也正是你在服务器 API 中使用和实现接口的方式。

1
2
3
4
func main() {
db := database{"Go T-Shirt": 25, "Go Jacket": 55}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}

使用 http.Handle 函数时,可以在服务器 API 中找到接口的其他用例。 有关详细信息,请参阅 Go 网站上编写 Web 应用程序帖子。

并发 *

在开始了解 Go 中并发的工作原理之前,你可能需要忘记从其他编程语言中已经了解的知识。 Go 使用的方法不同。

  • 并发 是指在同一个时间点上只能执行同一个任务,但是因为速度非常快,所以就像同时进行一样。
  • 并行 是指在一个时间点上同时处理多个任务。真正的并行,是需要电脑硬件的支持,单核的CPU是无法达到并行的。并行,他不一定快因为并行运行时是需要通信的,这种通信的成本还是很高的,而并发的程序成本很低。
  • 进程 就是一个独立功能的程序,在一个数据集中的一次动态执行过程,可以认为他是一个正在执行的程序,比如打开一个QQ就是在运行一个进程。
  • 线程 线程是被包含在进程之中的,它是比进程更小的能独立运行的基本单位 一个进程可以包含多个线程。例如、打开文档在你输入文字的时候他还在后台检测你输入的文字的大小写,还有拼写是否正确 ,这就是一个线程来检测的。
  • 协程 协程属于一种轻量级的线程,英文名 Goroutine 协程之间的调度由 Go运行时(runtime)管理。

了解 goruntine(轻量线程) *

并发是独立活动的组合,就像 Web 服务器虽然同时处理多个用户请求,但它是自主运行的。
Go 有两种编写并发程序的样式。 一种是在其他语言中通过线程实现的传统样式。一种是—

在本模块中,你将了解 Go 的样式,其中值是在称为 goroutine 的独立活动之间传递的,以与进程进行通信。

Go 实现并发的方法

通常,编写并发程序时最大的问题是在进程之间共享数据。但 Go 是通过 channel 来回传递数据的。 此方法意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。

Goroutine

goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。
使用 go 关键字调用这个函数开启一个 goroutine 时候,即使这个函数有返回值也会忽略。goroutine 调用之后会立即返回,不会等待 goroutine 的执行结果,所以 goroutine 不会接收返回值。 把封装main函数的goroutine叫做主 goroutine,main 函数作为主 goroutine 执行,如果 main 函数中 goroutine 终止了,程序也将终止,其他的 goroutine 都不会再执行。
假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 goroutine 同时调用这两个函数。
我们可以说,程序执行的第一个 goroutine 是 main() 函数。 如果要创建其他 goroutine,则必须在调用该函数之前使用 go 关键字,如下所示:

1
2
3
4
func main(){
login()
go launch()
}

你还会发现,许多程序喜欢使用匿名函数来创建 goroutine,在函数最后加上 () 后会直接执行:

1
2
3
4
5
6
func main(){
login()
go func() {
launch()
}()
}

为了查看运行中的 goroutine,让我们编写一个并发程序。

runtime包

其实go是运行在runtime调度器上的,它主要负责内存管理、垃圾回收、栈处理等等。也包含了Go运行时系统交互的操作,控制goroutine的操作,Go程序的调度器可以很合理的分配CPU资源给每一个任务。
Go1.5版本之前默认是单核执行的。从1.5之后使用可以通过runtime.GOMAXPROCS()来设置让程序并发执行,提高CPU的利用率。

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
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
//获取当前GOROOT目录
fmt.Println("GOROOT:", runtime.GOROOT())
//获取当前操作系统
fmt.Println("操作系统:", runtime.GOOS)
//获取当前逻辑CPU数量
fmt.Println("逻辑CPU数量:", runtime.NumCPU())

//设置最大的可同时使用的CPU核数 取逻辑cpu数量
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n) //一般在使用之前就将cpu数量设置好 所以最好放在init函数内执行

//goexit 终止当前goroutine
//创建一个goroutine
go func() {
fmt.Println("start...")
runtime.Goexit() //终止当前goroutine
fmt.Println("end...")
}()
time.Sleep(3 * time.Second) //主goroutine 休眠3秒 让子goroutine执行完
fmt.Println("main_end...")
}

调用runtime.Goexit()函数之后,会立即停止当前 goroutine,其他的 goroutine 不会受影响。并且当前 goroutine 如果有未执行的 defer 还是会执行完 defer 操作。需要注意的是 不能runtime.goexit() 放在主 goroutine (main函数)中执行,否则会发生运行时恐慌。

编写并发程序

由于我们只想将重点放在并发部分,因此使用现有程序来检查 API 终结点是否响应。

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
package main

import (
"fmt"
"net/http"
"time"
)

func main() {
start := time.Now()

apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}

for _, api := range apis {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
continue
}

fmt.Printf("SUCCESS: %s is up and running!\n", api)
}

elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
1
2
3
4
5
6
7
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!

这里没有什么特别之处,但我们可以做得更好。 或许我们可以同时检查所有站点? 此程序可以在 500 毫秒的时间内完成,不需要耗费将近两秒。
请注意,我们需要并发运行的代码部分是向站点进行 HTTP 调用的部分。 换句话说,我们需要为程序要检查的每个 API 创建一个 goroutine。
为了创建 goroutine,我们需要在调用函数前使用 go 关键字。 但我们在这里没有函数。 让我们重构该代码并创建一个新函数,如下所示:

1
2
3
4
5
6
7
8
func checkAPI(api string) {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
return
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}

注意,我们不再需要 continue 关键字,因为我们不在 for 循环中。 要停止函数的执行流,只需使用 return 关键字。 现在,我们需要修改 main() 函数中的代码,为每个 API 创建一个 goroutine,如下所示:

1
2
3
for _, api := range apis {
go checkAPI(api)
}

重新运行程序,看看发生了什么。
看起来程序不再检查 API 了,对吗? 显示的内容可能与以下输出类似:

1
Done! It took 1.506e-05 seconds!

速度可真快! 发生了什么情况? 你会看到最后一条消息,指出程序已完成,因为 Go 为循环中的每个站点创建了一个 goroutine,并且它立即转到下一行。
即使看起来 checkAPI 函数没有运行,它实际上是在运行。 它只是没有时间完成。 请注意,如果在循环之后添加一个睡眠计时器会发生什么,如下所示:

1
2
3
4
5
for _, api := range apis {
go checkAPI(api)
}

time.Sleep(3 * time.Second)
1
2
3
4
5
6
7
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!

看起来似乎起作用了,对吧? 不完全如此。 如果你想在列表中添加一个新站点呢? 也许三秒钟是不够的。 你怎么知道? 你无法管理。 必须有更好的方法,这就是我们在下一节讨论 channel 时要涉及的内容。

将 channel 用作通信机制

Go 中的 channel 是 goroutine 之间的通信机制。 请记住 Go 的并发方法是:“不是通过共享内存通信;而是通过通信共享内存。”当你需要将值从一个 goroutine 发送到另一个时,可以使用通道。

Channel 语法

由于 channel 是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送 channel 支持的数据类型。 除使用关键字 chan 作为 channel 的数据类型外,还需指定将通过 channel 传递的数据类型,如 int 类型。
每次声明一个 channel 或希望在函数中指定一个 channel 作为参数时,都需要使用 chan <type>,如 chan int。 若要创建通道,需使用内置的 make() 函数:

1
ch := make(chan int)

一个 channel 可以执行两项操作:发送数据和接收数据。 若要指定 channel 具有的操作类型,需要使用 channel 运算符 <-。 此外,在 channel 中发送数据和接收数据属于阻止操作。 你一会儿就会明白为何如此。
如果希望通道仅发送数据,请在通道之后使用 <- 运算符。 如果希望通道接收数据,请在通道之前使用 <- 运算符,如下所示:

1
2
3
ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

可在 channel 中执行的另一项操作是关闭 channel。 若要关闭通道,使用内置的 close() 函数:

1
close(ch)

当你关闭通道时,你希望数据将不再在该通道中发送。 如果试图将数据发送到已关闭的 channel,则程序将发生严重错误。 如果试图通过已关闭的 channel 接收数据,则可以读取发送的所有数据。 随后的每次“读取”都将返回一个零值。
让我们回到之前创建的程序,然后使用通道来删除睡眠功能。 首先,让我们在 main 函数中创建一个字符串 channel,如下所示:

1
ch := make(chan string)

接下来,删除睡眠行 time.Sleep(3 * time.Second)
现在,我们可以使用 channel 在 goroutine 之间进行通信。 应重构代码并通过通道发送该消息,而不是在 checkAPI 函数中打印结果。 要使用该函数中的 channel,需要添加 channel 作为参数。 checkAPI 函数应如下所示:

1
2
3
4
5
6
7
8
9
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}

ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

请注意,我们必须使用 fmt.Sprintf 函数,因为我们不想打印任何文本,只需利用通道发送格式化文本。 另请注意,我们在 channel 变量之后使用 <- 运算符来发送数据。
现在,你需要更改 main 函数以发送 channel 变量并接收要打印的数据,如下所示:

1
2
3
4
5
6
7
ch := make(chan string)

for _, api := range apis {
go checkAPI(api, ch)
}

fmt.Print(<-ch)

请注意,我们在 channel 之前使用 <- 运算符来表明我们想要从 channel 读取数据。
重新运行程序时,会看到如下所示的输出:

1
2
3
ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!

至少它不用调用睡眠函数就可以工作,对吧? 但它仍然没有达到我们的目的。 我们只看到其中一个 goroutine 的输出,而我们共创建了五个 goroutine。 在下一节中,我们来看看这个程序为什么是这样工作的。

无缓冲 channel

了解有缓冲 channel