微软Go学习教程(上半部分)
0.下载Go
go version #查看go版本,是否安装成功.
go env #查看环境变量
1.Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
go run main.go
编译并执行Go应用。
go build main.go
编译并生成可执行文件
2.声明和使用变量
package main
import "fmt"
func main(){
var a string = "Harris" //变量 名称 类型 初始化
var aa,bb int = 12,13
var (
b = 12 //自动推断类型
c = "Jack"
)
age := 123 //最常用的声明和初始化方式
name := "Lisa"
fmt.Println(a)
fmt.Println(b)
fmt.Println(c,aa,bb)
fmt.Println(age,name)
}
Harris
12
Jack
需要记住的重要一点是,在 Go 中,当你声明一个变量但不使用它时,Go 会抛出错误,而不是像某些其他编程语言一样抛出警告。
声明常量
常量和变量之间既有相似之处,也有一些重要差异。 例如,你可以在不使用常量的情况下声明常量。 你不会收到错误消息。 不能使用冒号等于号来声明常量。 如果采用这种方式,Go 会发出警告。
const (
StatusOK = 0
StatusConnectionReset = 1
StatusOtherError = 2
)
3.数据类型
Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。
Go 有四类数据类型:
- 基本类型:数字、字符串和布尔值
- 聚合类型:数组和结构
- 引用类型:指针、切片、映射、函数和通道
- 接口类型:接口
整型数字
一般来说,定义整数类型的关键字是 int
。 但 Go 还提供了 int8
、int16
、int32
和 int64
类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int
,则大小通常为 32 位。 在 64 位系统上,int
大小通常为 64 位。 但是,此行为可能因计算机而不同。 可以使用 uint
。 但是,只有在出于某种原因需要将值表示为无符号数字的情况下,才使用此类型。 此外,Go 还提供 uint8
、uint16
、uint32
和 uint64
类型。
var integer8 int8 = 127
var integer16 int16 = 32767
var integer32 int32 = 2147483647
var integer64 int64 = 9223372036854775807
fmt.Println(integer8, integer16, integer32, integer64)
大多数情况下,你将使用 int
,但需要了解其他整数类型,因为在 Go 中,int
与 int32
不同,即使整数的自然大小为 32 位也是如此。 换句话说,需要强制转换时,你需要进行显式转换。 如果尝试在不同类型之间执行数学运算,将会出现错误。 例如,假定你具有下面的代码:
运行该程序时,你会收到以下错误:
invalid operation: integer16 + integer32 (mismatched types int16 and int32)
如你所见,在 Go 中将值从一种类型转换为另一种类型时,需要显式声明新类型。 我们将在本模块结束时讨论如何正确地强制转换类型。
在学习 Go 过程中,你可能会收到有关 runes 的信息。 rune
只是 int32
数据类型的别名。 它用于表示 Unicode 字符(或 Unicode 码位)。 例如,假设有以下代码:
rune := 'G'
fmt.Println(rune)
运行前面的代码片段时,你可能会在命令提示符下看到程序打印符 G
。 不过,你还会看到数字 71
,它表示 G
的 Unicode 字符。 我们将在后续模块中详细介绍 runes。
内置builtin.go 查看数据类型
https://go.dev/src/builtin/builtin.go
浮点数
var float32 float32 = 2147483647
var float64 float64 = 9223372036854775807
fmt.Println(float32, float64)
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.MaxFloat32, math.MaxFloat64)
}
布尔型
布尔类型仅可能有两个值:true
和 false
。 你可以使用关键字 bool
声明布尔类型。 Go 不同于其他编程语言,在 Go 中,你不能将布尔类型隐式转换为 0 或 1。 你必须显式执行此操作。
因此,你可以按如下方式声明布尔变量:
var featureFlag bool = true
最后,让我们看一下编程语言中最常见的数据类型:string。 在 Go 中,关键字 string
用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号("
)中定义值。 单引号('
)用于单个字符(以及 runes,正如我们在上一节所述)。
例如,下面的代码演示了声明和初始化字符串变量的两种方法:
package main
import (
"fmt"
)
func main() {
var firstName string = "John"
lastName := "Doe"
fmt.Println(firstName, lastName)
}
有时,你需要对字符进行转义。 为此,在 Go 中,请在字符之前使用反斜杠 (\
)。 例如,下面是使用转义字符的最常见示例:
-
\n
:新行 -
\r
:回车符 -
\t
:选项卡 -
\'
:单引号 -
\"
:双引号 -
\\
:反斜杠
默认值
到目前为止,几乎每次声明变量时,都使用值对其进行了初始化。 但与在其他编程语言中不同的是,在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。
下面列出了我们目前浏览过类型的几个默认值:
-
int
类型的 0
(及其所有子类型,如 int64
) -
float32
和 float64
类型的 +0.000000e+000
-
bool
类型的 false
-
string
类型的空值
类型转换
在上一节中,我们确认在 Go 中隐式强制转换不起作用。 接下来,需要显式强制转换。 Go 提供了将一种数据类型转换为另一种数据类型的一些本机方法。 例如,一种方法是对每个类型使用内置函数,如下所示:
package main
import (
"fmt"
)
func main() {
var integer16 int16 = 127
var integer32 int32 = 32767
fmt.Println(int32(integer16) + integer32)
}
Go 的另一种转换方法是使用 strconv 包。 例如,若要将 string
转换为 int
,可以使用以下代码,反之亦然:
package main
import (
"fmt"
"strconv"
)
func main() {
i, _ := strconv.Atoi("-42")
s := strconv.Itoa(-42)
fmt.Println(i, s)
}
main函数
与之交互的函数是 main()
函数。 Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 你的程序中只能有一个 main()
函数。 如果创建的是 Go 包,则无需编写 main()
函数。 我们将在后续模块中介绍如何创建包。
在深入了解如何创建自定义函数的基本知识之前,让我们看看 main()
函数的一个重要特性。 你可能留意到,main()
函数没有任何参数,并且不返回任何内容。 但这并不意味着其不能从用户读取值,如命令行参数。 如要访问 Go 中的命令行参数,可以使用用于保存传递到程序的所有参数的 os 包 和 os.Args
变量来执行操作。
下面的代码从命令行读取两个数字,并为其求和:
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
以进行求和。
若要运行程序,请使用以下命令:
go run main.go 3 5
4.自定义函数
func name(parameters) (results) {
body-content
}
请注意,使用 func
关键字来定义函数,然后为其指定名称。 在命名后,指定函数的参数列表。 你可以指定零个或多个参数。 你还可以定义函数的返回类型,该函数也可以是零个或多个。 (我们将在下一节中讨论如何返回多个值)。在定义所有这些值之后,你可以编写函数的正文内容。
若要练习此技巧,我们将重构上一节的代码,为自定义函数中的数字求和。 我们将使用以下代码:
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
}
在 Go 中,你还可以为函数的返回值设置名称,将其当作一个变量。 例如,你可以重构如下 sum
函数:
func sum(number1 string, number2 string) (result int) {
int1, _ := strconv.Atoi(number1)
int2, _ := strconv.Atoi(number2)
result = int1 + int2
return
}
请注意,你现在需要将函数的结果值括在括号中。 你还可以在函数中使用该变量,并且只需在末尾添加 return
行。 Go 将返回这些返回变量的当前值。 在函数末尾编写 return
关键字非常简单方便,尤其是在有多个返回值时。 我们不建议使用此方法。 可能不确定函数将返回什么。
返回多个值
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 的另一个有趣功能是,如果不需要函数的某个返回值,可以通过将返回值分配给 _
变量来放弃该函数。 _
变量是 Go 忽略返回值的惯用方式。 它允许程序进行编译。 因此,如果只需要求和,则可以使用以下代码:
package main
import "fmt"
func main() {
sum, _ := calc(os.Args[1], os.Args[2])
fmt.Println("Sum:", sum)
}
更改函数参数值
将值传递给函数时,该函数中的每个更改都不会影响调用方。 Go 是“按值传递”编程语言。 每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。 在函数中对该变量所做的更改都不会影响你向函数发送的更改。
例如,假设你创建了一个用于更新人员姓名的函数。 请注意,运行此代码时会发生的变化:
package main
import "fmt"
func main() {
firstName := "John"
updateName(firstName)
fmt.Println(firstName)
}
func updateName(name string) {
name = "David"
}
即使你在函数中将该名称更改为 David,输出仍为 John。 由于 updateName
函数中的更改仅会修改本地副本,因此输出不会发生变化。 Go 传递变量的值,而不是变量本身。
如果你希望在 updateName
函数中进行的更改会影响 main
函数中的 firstName
变量,则需要使用指针。 指针 是包含另一个变量的内存地址的变量。 当你发送指向某个函数的指针时,不会传递值,而是传递地址内存。 因此,对该变量所做的每个更改都会影响调用方。
在 Go 中,有两个运算符可用于处理指针:
-
&
运算符使用其后对象的地址。 -
*
运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。
让我们修改前面的示例,以阐明指针的工作方式:
package main
import "fmt"
func main() {
firstName := "John"
updateName(&firstName)
fmt.Println(firstName)
}
func updateName(name *string) {
*name = "David"
}
首先要做的就是修改函数的签名,以指明你要接收指针。 为此,请将参数类型从 string
更改为 *string
。 (后者仍是字符串,但现在它是指向字符串 的 指针。)然后,将新值分配给该变量时,需要在该变量的左侧添加星号 (*
) 以暂停该变量的值。 调用 updateName
函数时,系统不会发送值,而是发送变量的内存地址。 这就是前面的代码在变量左侧带有 &
符号的原因。
引用本地包
calculator包下初始化go.mod
go mod init github.com/myuser/calculator
文件目录结构
src/
calculator/
go.mod
sum.go
var_use/
main.go
var_use下初始化go.mod
go mod init var_use
module var_use
go 1.17
require github.com/myuser/calculator v0.0.0
replace github.com/myuser/calculator => ../calculator //本地路径下的位置
总结
我们介绍了开始在 Go 中构建更复杂应用程序所需的基础知识。 现在,你可以通过几种方法来声明和初始化变量。 你还了解 Go 提供的各种数据类型。 你已经使用最基本的数据类型,
并且还了解到如何创建函数来组织代码,并使代码更易于维护。 你已经了解到,Go 是“按值传递”语言,但它亦支持指针。 我们将在后续的部分模块中使用指针。
最后,你已经了解包在 Go 中的工作原理,以及如何在需要与其他开发人员共享代码时创建模块。 你已经了解到,如果只需要创建独立的应用程序,则所有代码都需要成为 main
包的一部分。 你已经了解到,该程序的起点是 main()
函数。 现在,你已了解如何引用本地和第三方模块。 构建 Go 程序时,你将会使用这些模块。
5.条件语句
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")
}
}
在 Go 中,在 if
块内声明变量是惯用的方式。 这是一种使用在 Go 中常见的约定进行高效编程的方式。
switch
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
sec := time.Now().Unix()
rand.Seed(sec)
i := rand.Int31n(3)
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
}
fmt.Println("ok")
}
如果多次运行前面的代码,则每次都会看到不同的输出。 (但是,如果在 Go Playground 中运行代码,则每次都会获得相同的结果。 这是此服务的局限性之一。)
Go 会执行 switch
语句的每个用例,直到找到条件的匹配项。 但请注意,前面的代码未涵盖 num
变量值的所有可能情况。 如果 num
最终为 5
,则程序的输出为 ok
。
也可让默认用例更加具体,像下面这样包含它:
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
default:
fmt.Print("no match...")
}
请注意,对于 default
用例,不要编写验证表达式, 只需包含 i
变量即可,因为你将在 case
语句中验证其值。
使用多个表达式
有时,多个表达式仅与一个 case
语句匹配。 在 Go 中,如果希望 case
语句包含多个表达式,请使用逗号 (,
) 来分隔表达式。 此方法可避免代码重复。
以下代码示例演示了如何包含多个表达式。
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
语句。 例如,以下代码调用 time.Now()
函数。 它提供的输出取决于当前工作日。
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 weekend, time to rest!")
}
fmt.Println(time.Now().Weekday().String())
}
从 switch
语句调用函数时,无需更改表达式即可修改其逻辑,因为你始终会验证函数返回的内容。
此外,还可以从 case
语句调用函数。 例如,使用此方法可以通过正则表达式来匹配特定模式。 下面是一个示例:
package main
import "fmt"
import "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")
}
}
请注意,switch
块没有任何验证表达式。 我们将在下一部分讨论该概念。
在 Go 中,可以在 switch
语句中省略条件,就像在 if
语句中那样。 此模式类似于比较 true
值,就像强制 switch
语句一直运行一样。
下面是一个示例,说明了如何编写不带条件的 switch
语句:
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")
}
}
该程序始终运行这种类型的 switch
语句,因为条件始终为 true。 一个条件 switch
块比一长串的 if
和 else if
语句更易于维护。
使逻辑进入到下一个 case
在某些编程语言中,你会在每个 case
语句末尾写一个 break
关键字。 但在 Go 中,当逻辑进入某个 case 时,它会退出 switch
块,除非你显式停止它。 若要使逻辑进入到下一个紧邻的 case,请使用 fallthrough
关键字。
若要更好地了解此模式,请查看以下代码示例。
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)
}
}
15 is less than 50
15 is greater than 100
15 is less than 200
请注意,由于 num
为 15(小于 50),因此它与第一个 case 匹配。 但是,num
不大于 100。 由于第一个 case
语句包含 fallthrough
关键字,因此逻辑会立即转到下一个 case
语句,而不会对该 case 进行验证。 因此,在使用 fallthrough
关键字时必须谨慎。 该代码产生的行为可能不是你想要的。
for循环
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum of 1..100 is", sum)
}
在某些编程语言中,可以使用 while
关键字编写循环模式,在这些模式中,只有条件表达式是必需的。 Go 没有 while
关键字。 但是,你可以改用 for
循环。 此预配使预处理语句和后处理语句变得可选。
使用以下代码片段确认是否可以在不使用预处理语句和后处理语句的情况下使用 for
循环。
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)
}
}
只要 num
变量保存的值与 5
不同,程序就会输出一个随机数。
可以在 Go 中编写的另一种循环模式是无限循环。 在这种情况下,你不编写条件表达式,也不编写预处理语句或后处理语句, 而是采取退出循环的方式进行编写。 否则,逻辑永远都不会退出。 若要使逻辑退出循环,请使用 break
关键字。
若要正确编写无限循环,请在 for
关键字后面使用大括号,如下所示:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var num int32
sec := time.Now().Unix()
rand.Seed(sec)
for {
fmt.Print("Writting inside the loop...")
if num = rand.Int31n(10); num == 5 {
fmt.Println("finish!")
break
}
fmt.Println(num)
}
}
Continue 语句
在 Go 中,可以使用 continue
关键字跳过循环的当前迭代。 例如,可以使用此关键字在循环继续之前运行验证。 也可以在编写无限循环并需要等待资源变得可用时使用它。
此示例使用 continue
关键字:
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)
}
6.defer和panic和recover进行控制
在 Go 中,defer
语句会推迟函数(包括任何参数)的运行,直到包含 defer
语句的函数完成。 通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟某个函数的运行。
可以根据需要推迟任意多个函数。 defer 语句按逆序运行,先运行最后一个,最后运行第一个。
通过运行以下示例代码来查看此模式的工作原理:
package main
import "fmt"
func main() {
for i := 1; i <= 4; i++ {
defer fmt.Println("deferred", -i)
fmt.Println("regular", i)
}
}
输出:
regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1
在此示例中,请注意,每次推迟 fmt.Println("deferred", -i)
时,都会存储 i
的值,并会将其运行任务添加到栈中。 在 main()
函数输出完 regular
值后,所有推迟的调用都会运行。 这就是你看到输出采用逆序(后进先出)的原因。
defer
函数的一个典型用例是在使用完文件后将其关闭。 下面是一个示例:
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()
函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。
例如,下面的代码将 panic
和 defer
函数组合在一起。 尝试运行此代码以了解控制流的中断。 请注意,清理过程仍会运行。
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!")
}
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(0x866760, 0xc0000ce008)
D:/Go_Study/MS_Study/var_use/main.go:8 +0x285
main.highlow(0x2, 0x2)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.highlow(0x2, 0x1)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.highlow(0x2, 0x0)
D:/Go_Study/MS_Study/var_use/main.go:13 +0x20f
main.main()
D:/Go_Study/MS_Study/var_use/main.go:17 +0x25
Process finished with the exit code 2
下面是运行代码时会发生的情况:
- 一切正常运行。 程序将输出传递到
highlow()
函数中的高值和低值。 - 如果
low
的值大于 high
的值,则程序会崩溃。 会显示“Panic!
”消息。 此时,控制流中断,所有推迟的函数都开始输出“Deferred...
”消息。 - 程序崩溃,并显示完整的堆栈跟踪。 不会显示“
Program finished successfully!
”消息。
在发生未预料到的严重错误时,系统通常会运行对 panic()
函数的调用。 若要避免程序崩溃,可以使用名为 recover()
的另一个函数。
recover函数
func main() {
defer func() {
handler := recover()
if handler != nil {
fmt.Println("main(): recover", handler)
}
}()
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
运行程序时,输出应该如下所示:
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.
你是否看到了相对于上一版本的差异? 主要差异在于,你不再看到堆栈跟踪错误。
在 main()
函数中,你会将一个可以调用 recover()
函数的匿名函数推迟。 当程序处于紧急状态时,对 recover()
的调用无法返回 nil
。 你可以在此处执行一些操作来清理混乱,但在本例中,你只是简单地输出一些内容。
panic
和 recover
函数的组合是 Go 处理异常的惯用方式。 其他编程语言使用 try/catch
块。 Go 首选此处所述的方法。
小练习
首先,编写一个用于输出数字(1 到 100)的程序,其中有以下变化:
- 如果数字可被 3 整除,则输出
Fizz
。 - 如果数字可被 5 整除,则输出
Buzz
。 - 如果数字可同时被 3 和 5 整除,则输出
FizzBuzz
。 - 如果前面的情况都不符合,则输出该数字。
尝试使用 switch
语句。
package main
import "fmt"
func main(){
for i:=1;i<=100;i++ {
switch {
case i%3 == 0 && i%5 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
7.数组和切片
在前面的模块中,我们介绍了 Go 中的基本数据类型,它们是在 Go 程序中构建数据结构的基础。 在本模块中,我们将介绍前面提到的聚合类型:数组和切片。 你将了解这两者之间的区别以及何时选择一种类型而弃用另一种。 你还将了解映射和结构,它们是使用 Go 操作数据的基础,并用于许多 Web 服务。
最后,你将学习如何构建自定义和复杂的数据类型,它们在分析 JSON 有效负载时将非常有用。
使用数组
Go 中的数组是一种特定类型且长度固定的数据结构。 它们可具有零个或多个元素,你必须在声明或初始化它们时定义大小。 此外,它们一旦创建,就无法调整大小。 鉴于这些原因,数组在 Go 程序中并不常用,但它们是切片和映射的基础。
要在 Go 中声明数组,必须定义其元素的数据类型以及该数组可容纳的元素数目。 然后,可采用下标表示法访问数组中的每个元素,其中第一个元素是 0,最后一个元素是数组长度减去 1(长度 - 1)。
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 会用默认数据类型初始化每个元素。 这样的话,int
的默认值为零。 不过,你可为特定位置分配值。 这就是为什么你会看到 a[1] = 10
。 你可采用上述表示法来访问该元素。 另请注意,为了打印出第一个元素,我们使用了 a[0]
。 为了打印出最后一个元素,我们使用了 a[len(a)-1]
。 len
函数是 Go 中的内置函数,用于获取数组、切片或映射中的元素数。
声明数组时,还可使用非默认值来初始化数组。 例如,你可使用以下代码来查看和测试语法:
package main
import "fmt"
func main() {
cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}
fmt.Println("Cities:", cities)
}
数组中的省略号
如果你不知道你将需要多少个位置,但知道你将具有多少数据,那么还有一种声明和初始化数组的方法是使用省略号 (...
),如下例所示:
q := [...]int{1, 2, 3}
另一种有趣的数组初始化方法是使用省略号并仅为最新的位置指定值。 例如,使用以下代码:
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))
}
请注意数组的长度是 100,因为你为第 99 个位置指定了一个值。 第一个位置打印出默认值(零)。
First Position: 0
Last Position: -1
Length: 100
多维数组
如果需要处理复杂数据结构,请记住 Go 支持多维数组。 让我们创建一个程序,在其中声明和初始化一个二维数组。 使用以下代码:
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)
}
切片
我们在上一部分了解了数组,并了解了数组是切片和映射的基础。 你稍后就会明白是为什么。 与数组一样,切片也是 Go 中的一种数据类型,它表示一系列类型相同的元素。 不过,与数组更重要的区别是切片的大小是动态的,不是固定的。
切片是数组或另一个切片之上的数据结构。 我们将源数组或切片称为基础数组。 通过切片,可访问整个基础数组,也可仅访问部分元素。
片只有 3 个组件:
- 指向基础数组中第一个可访问元素的指针。 此元素不一定是数组的第一个元素
array[0]
。 - 切片的长度。 切片中的元素数目。
- 切片的容量。 切片开头与基础数组结束之间的元素数目。
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[:7]
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}
[April May June] 3 9
[April May June July August September October] 7 9
追加项
我们了解了切片的工作原理,还学习了它们与数组的相似性。 现在,让我们来了解它们与数组之间有何不同。 第一个区别是切片的大小不是固定的,而是动态的。 创建切片后,可向其添加更多元素,这样切片就会扩展。 稍后你将了解基础数组发生的情况。
Go 提供了内置函数 append(slice, element)
,便于你向切片添加元素。 将要修改的切片和要追加的元素作为值发送给该函数。 然后,append
函数会返回一个新的切片,将其存储在变量中。 对于要更改的切片,变量可能相同。
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)
}
}
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]
来新建一个仅包含所需元素的切片。
例如,以下代码会从切片中删除元素:
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)
}
}
创建切片的副本
Go 具有内置函数 copy(dst, src []Type)
用于创建切片的副本。 你需要发送目标切片和源切片。 例如,你可如下例所示创建一个切片副本:
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
为何要创建副本? 更改切片中的元素时,基础数组将随之更改。 引用该基础数组的任何其他切片都会受到影响。 让我们在代码中看看此过程,然后创建一个切片副本来解决此问题。
使用下述代码确认切片指向数组,而你在切片中所做的每个更改都会影响基础数组。
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)
}
Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]
请注意对 slice1
所做的更改如何影响 letters
数组和 slice2
。 可在输出中看到字母 B 已替换为 Z,它会影响指向 letters
数组的每个切片。
若要解决此问题,你需要创建一个切片副本,它会在后台生成新的基础数组。 可以使用以下代码:
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)
}
Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]
请注意 slice1
中的更改如何影响基础数组,但它并未影响新的 slice2
。
8.映射
大体上来说,Go 中的映射是一个哈希表,是键值对的集合。 映射中所有的键都必须具有相同的类型,它们的值也是如此。 不过,可对键和值使用不同的类型。 例如,键可以是数字,值可以是字符串。 若要访问映射中的特定项,可引用该项的键。
声明和初始化映射
若要声明映射,需要使用 map
关键字。 然后,定义键和值类型,如下所示:map[T]T
。 例如,如果要创建一个包含学生年龄的映射,可使用以下代码:
package main
import "fmt"
func main() {
studentsAge := map[string]int{
"john": 32,
"bob": 31,
}
fmt.Println(studentsAge)
}
如果不想使用项来初始化映射,可使用内置函数 make()
在上一部分创建切片。 可使用以下代码创建空映射:
studentsAge := make(map[string]int)
映射是动态的。 创建项后,可添加、访问或删除这些项。 让我们来了解这些操作。
添加项
要添加项,无需像对切片一样使用内置函数。 映射更加简单。 你只需定义键和值即可。 如果没有键值对,则该项会添加到映射中。
让我们使用 make
函数重写之前用于创建映射的代码。 然后,将项添加到映射中。 可以使用以下代码:
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
这里用studentsAge := map[string]int{}
也可以创建空映射,而不是nil映射。
请注意,我们已向已初始化的映射添加了项。 但如果尝试使用 nil
映射执行相同操作,会出现错误。 例如,以下代码将不起作用:
package main
import "fmt"
func main() {
var studentsAge map[string]int
studentsAge["john"] = 32
studentsAge["bob"] = 31
fmt.Println(studentsAge)
}
在映射中使用下标表示法时,即使映射中没有键,你也总会获得响应。 当你访问不存在的项时,Go 不会执行 panic。 此时,你会获得默认值。 可使用以下代码来确认该行为:
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"])
}
在很多情况下,访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。
若要解决上一代码片段遇到的问题,可执行以下代码:
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")
}
}
使用第二个代码片段检查映射中的键在你访问之前是否存在。
删除项
若要从映射中删除项,请使用内置函数 delete()
。 下例演示了如何从映射
package main
import "fmt"
func main() {
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31
delete(studentsAge, "john")
fmt.Println(studentsAge)
}
正如上述所言,如果你尝试删除不存在的项,Go 不会执行 panic。
映射中的循环
最后,让我们看看如何在映射中进行循环来以编程方式访问其所有的项。 为此,可使用基于范围的循环,如下例所示:
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)
}
}