简介
在上一模块中,你已了解 Go 的基础知识。 你探索了数据类型、变量、常量、函数和包。 你知道如何构建程序的逻辑,以及 Go 如何首选对代码进行组织。 因此,现在你已准备好学习如何使用 if/else
、switches
、for
之类的基本控制流和另一组关键字以 Go 方式编写程序。
如果你以前有过遵循大多数 C
语法的编程语言(例如 C#
或 Java
)的经验,则此模块中的几个部分会非常简单。 但在其他部分,你会看到 Go 中存在的细微差别。
在本模块快要结束的时候,你会发现一个需要应用你目前已学习的所有概念的挑战。 你可以稍后将该挑战的解决方案与你自己的解决方法进行比较。
学习目标
在本模块中,你将学习以下内容:
- 了解简单的和复合的 if 语句。
- 了解 switch 语句及其功能。
- 使用 for 关键字来了解 loop 语句及其在 Go 中的模式。
- 使用基本函数来处理 defer、panic 和 recover 之类的错误。
先决条件
一个已准备好创建应用程序的 Go 环境。
- 了解如何创建和修改 .go 文件。
- 了解如何使用终端提示符运行 Go 应用程序。
- 了解如何声明和初始化变量。
- 了解如何创建函数。
使用 if 和 else 语句
在任何编程语言中,最基本的控制流都是 if/else
语句。 在 Go 中,if/else
语句非常简单。 但是,你需要先了解一些差异,然后才能得心应手地编写 Go 程序。
让我们看看 if 语句的 Go 语法。
if 语句的语法
与其他编程语言不同的是,在 Go 中,你不需要在条件中使用括号。 else
子句可选。 但是,大括号仍然是必需的。 此外,为了减少行,Go 不支持三元 if 语句,因此每次都需要编写完整的 if 语句。
下面是 if 语句的一个基本示例:
package main
import "fmt"
func main() {
x := 27
if x%2 == 0 {
fmt.Println(x, "is even")
}
}
复合 if 语句
Go 支持复合 if
语句。 可以使用 else if
语句对语句进行嵌套。 下面是一个示例:
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
分支中可用。 但是,如果尝试在 if
块之外输出 num
变量的值,则会出现如下错误:
package main
import "fmt"
func somenumber() int {
return -7
}
func main() {
if num := somenumber(); num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
fmt.Println(num)
}
运行程序时,错误输出如下所示:
# command-line-arguments
./main.go:17:14: undefined: num
在 Go 中,在 if
块内声明变量是惯用的方式,也就是说,它是一种使用在 Go 中常见的约定进行高效编程的方式。
使用 switch 语句
像其他编程语言一样,Go 支持 switch
语句。 可以使用 switch
语句来避免链接多个 if
语句。 使用 switch
语句,就不需维护和读取包含多个 if
语句的代码。 这些语句还可以让复杂的条件更易于构造。 请参阅以下部分的 switch
语句。
基本 switch 语法
像 if
语句一样,switch
条件不需要括号。 最简单形式的 switch
语句如下所示:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
sec := time.Now().Unix()
rand.Seed(sec)
i := rand.Int31n(10)
switch i {
case 0:
fmt.Print("zero...")
case 1:
fmt.Print("one...")
case 2:
fmt.Print("two...")
}
fmt.Println("ok")
}
如果多次运行前面的代码,则每次都会看到不同的输出。
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, %sn", region, continent)
}
请注意,在 case
语句的表达式中包含的值对应于 switch
语句验证的变量的数据类型。 如果包含一个整数值作为新的 case
语句,程序将不会进行编译。
调用函数
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")
}
}
可以通过此模式更清晰地编写较长的 if-then-else
链。
使逻辑进入到下一个 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 50n", num)
fallthrough
case num > 100:
fmt.Printf("%d is greater than 100n", 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 循环
另一个常用控制流是循环。 Go 只使用一个循环构造,即 for
循环。 但是,你可以通过多种方式表示循环。 此部分介绍 Go 支持的循环模式。
基本 for 循环语法
与 if
语句和 switch
语句一样,for
循环表达式不需要括号。 但是,大括号是必需的。
分号 (;)
分隔 for
循环的三个组件:
- 在第一次迭代之前执行的初始语句(可选)。
- 在每次迭代之前计算的条件表达式。 该条件为 false 时,循环会停止。
- 在每次迭代结束时执行的后处理语句(可选)。
如你所见,Go 中的 for
循环类似于 C
、Java
和 C#
之类的编程语言中的 for
循环。
在 Go 中,最简单形式的 for
循环如下所示:
func main() {
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum of 1..100 is", sum)
}
让我们看看如何在 Go 中以其他方式编写循环。
空的预处理语句和后处理语句
在某些编程语言中,可以使用 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 不同,程序就会输出一个随机数。
无限循环和 break 语句
可以在 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)
}
此示例有一个 for
循环,该循环从 1
迭代到 100
,并在每次迭代中将当前数字添加到总和。 在此循环的当前迭代中,将跳过每个可被 5 整除的数字,不会将其添加到总和。
使用 defer、panic 和 recover 函数
现在,请考虑 Go 中一些不太常用的控制流:defer
、panic
和 recover
。 如你所见,Go 在某些方面采用惯用的方式,而下面这三种控制流则是 Go 独有的。
其中的每个函数都有几个用例。 你将在此处了解最重要的用例。 让我们开始学习第一个函数。
defer 函数
在 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"
)
func main() {
f, err := os.Create("notes.txt")
if err != nil {
return
}
defer f.Close()
if _, err = io.WriteString(f, "Learning Go!"); err != nil {
return
}
f.Sync()
}
创建或打开某个文件后,可以推迟 f.Close()
函数的执行,以免在你向文件中写入内容后忘记关闭该文件。
panic 函数
运行时错误会使 Go 程序进入紧急状态。 可以强制程序进入紧急状态,但运行时错误(例如数组访问超出范围、取消对空指针的引用)也可能会导致进入紧急状态。
内置 panic()
函数会停止正常的控制流。 所有推迟的函数调用都会正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误和堆栈跟踪,有助于诊断问题的根本原因。
调用 panic()
函数时,可以添加任何值作为参数。 通常,你会发送一条错误消息,说明为什么会进入紧急状态。
例如,将 panic
和 defer
函数组合起来,以了解控制流中断的方式。 但是,请继续运行任何清理进程。 请使用以下代码片段:
package main
import "fmt"
func main() {
g(0)
fmt.Println("Program finished successfully!")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic("Panic in g() (major)")
}
defer fmt.Println("Defer in g()", i)
fmt.Println("Printing in g()", i)
g(i + 1)
}
运行代码时,输出如下所示:
Printing in g() 0
Printing in g() 1
Printing in g() 2
Printing in g() 3
Panicking!
Defer in g() 3
Defer in g() 2
Defer in g() 1
Defer in g() 0
panic: Panic in g() (major)
goroutine 1 [running]:
main.g(0x4)
/Users/johndoe/go/src/helloworld/main.go:13 +0x22e
main.g(0x3)
/Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x2)
/Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x1)
/Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.g(0x0)
/Users/johndoe/go/src/helloworld/main.go:17 +0x17a
main.main()
/Users/johndoe/go/src/helloworld/main.go:6 +0x2a
exit status 2
下面是运行代码时会发生的情况:
一切正常运行。 程序输出 g()
函数接收的值。
当 i
大于 3 时,程序会进入紧急状态。 会显示“Panicking!”
消息。 此时,控制流中断,所有推迟的函数都开始输出“Defer in g()”
消息。
程序崩溃,并显示完整的堆栈跟踪。 不会显示“Program finished successfully!”
消息。
在发生未预料到的严重错误时,系统通常会运行对 panic()
的调用。 若要避免程序崩溃,可以使用 recover()
函数。 下一部分将介绍该函数。
recover 函数
有时,你可能想要避免程序崩溃,改为在内部报告错误。 或者,你可能想要先清理混乱情况,然后再让程序崩溃。 例如,你可能想要关闭与某个资源的连接,以免出现更多问题。
Go 提供内置函数 recover()
,允许你在出现紧急状况之后重新获得控制权。 只能在已推迟的函数中使用此函数。 如果调用 recover()
函数,则在正常运行的情况下,它会返回 nil
,没有任何其他作用。
请尝试修改前面的代码,添加对 recover()
函数的调用,如下所示:
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in main", r)
}
}()
g(0)
fmt.Println("Program finished successfully!")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic("Panic in g() (major)")
}
defer fmt.Println("Defer in g()", i)
fmt.Println("Printing in g()", i)
g(i + 1)
}
运行程序时,输出应该如下所示:
Printing in g() 0
Printing in g() 1
Printing in g() 2
Printing in g() 3
Panicking!
Defer in g() 3
Defer in g() 2
Defer in g() 1
Defer in g() 0
Recovered in main Panic in g() (major)
你是否看到了相对于上一版本的差异? 主要差异在于,你不再看到堆栈跟踪错误。
在 main()
函数中,你会将一个可以调用 recover()
函数的匿名函数推迟。 当程序处于紧急状态时,对 recover()
的调用无法返回 nil
。 你可以在此处执行一些操作来清理混乱,但在这种情况下,你可以直接输出一些内容。
panic
和 recover
的组合是 Go 处理异常的惯用方式。 其他编程语言使用 try/catch
块。 Go 首选此处所述的方法。
有关详细信息,请参阅在 Go 中添加内置 try
函数的建议。
使用控制流编写程序
通过完成一些编码挑战问题,练习你在此模块中了解的内容。 这些挑战问题并不复杂,你会在下一个单元中找到解决方案。
请首先尝试自行解决挑战问题。 然后,将你的结果与解决方案进行比较。 如果忘记了重要的详细信息,你始终可以查看该模块。
编写 FizzBuzz 程序
首先,编写一个用于输出数字(1 到 100)的程序,其中有以下变化:
- 如果数字可被 3 整除,则输出 Fizz。
- 如果数字可被 5 整除,则输出 Buzz。
- 如果数字可同时被 3 和 5 整除,则输出 FizzBuzz。
- 如果前面的情况都不符合,则输出该数字。
- 尝试使用 switch 语句。
推测平方根
编写一个程序来推测数字的平方根。 使用以下公式:
sroot = sroot − (sroot − x) / (2 * sroot)
此公式适用于牛顿的方法。
运行此公式的次数越多,就越接近数字的平方根。 将 sroot
变量初始化为 1
,无论你要查找其平方根的数字如何。 重复此计算最多 10
次,然后输出每次推测的结果。
你需要的计算次数可能少于 10
次。 如果获得的结果与前面的一次运行的结果相同,则可以停止循环。 数字越大,需要运行的计算次数越多。 为简单起见,你将最多重复计算 10
次。
例如,如果输入数字为 25
,则输出应如下所示:
`A guess for square root is 13
A guess for square root is 7.461538461538462
A guess for square root is 5.406026962727994
A guess for square root is 5.015247601944898
A guess for square root is 5.000023178253949
A guess for square root is 5.000000000053723
A guess for square root is 5
Square root is: 5`
要求用户输入一个数字,如果该数字为负数,则进入紧急状态
编写一个要求用户输入一个数字的程序。 在开始时使用以下代码片段:
package main
import (
"fmt"
)
func main() {
val := 0
fmt.Print("Enter number: ")
fmt.Scanf("%d", &val)
fmt.Println("You entered:", val)
}
此程序要求用户输入一个数字,然后将其输出。 修改示例代码,使之符合以下要求:
- 持续要求用户输入一个整数。 此循环的退出条件应该是用户输入了一个负数。
- 当用户输入负数时,让程序崩溃。 然后输出堆栈跟踪错误。
- 如果数字为 0,则输出“0 is neither negative nor positive”。 继续要求用户输入数字。
- 如果数字为正数,则输出“You entered: X”(其中的 X 为输入的数字)。 继续要求用户输入数字。
现在,请忽略用户输入的内容可能不是整数这种可能性。
解决方案 - 控制流挑战
下面是上述每个挑战的解决方案。
编写 FizzBuzz 程序
该挑战的解决方案可能如下所示:
package main
import (
"fmt"
"strconv"
)
func fizzbuzz(num int) string {
switch {
case num%15 == 0:
return "FizzBuzz"
case num%3 == 0:
return "Fizz"
case num%5 == 0:
return "Buzz"
}
return strconv.Itoa(num)
}
func main() {
for num := 1; num <= 100; num++ {
fmt.Println(fizzbuzz(num))
}
}
对于 FizzBuzz
用例,请将 3 乘以 5,因为结果可被 3 和 5 整除。 还可以包含一个 AND
条件来检查数字是否可被 3 和 5 整除。
推测平方根
该挑战的解决方案可能如下所示:
package main
import "fmt"
func sqrt(num float64) float64 {
currguess := 1.0
prevguess := 0.0
for count := 1; count <= 10; count++ {
prevguess = currguess
currguess = prevguess - (prevguess*prevguess-num)/(2*prevguess)
if currguess == prevguess {
break
}
fmt.Println("A guess for square root is ", currguess)
}
return currguess
}
func main() {
var num float64 = 25
fmt.Println("Square root is:", sqrt(num))
}
此解决方案在循环内包含一个 if
语句。 如果以前的数字和当前的数字相同,则此语句会停止该逻辑。 对于较大的数字,你需要的计算次数可能会超出 10 次。 因此,你可以更改代码,使用一个无限循环来运行计算,直到以前的推测结果和当前的推测结果相同。 为了避免小数精度问题,需要使用舍入数字。
要求用户输入一个数字,如果该数字为负数,则进入紧急状态
该挑战的解决方案可能如下所示:
package main
import (
"fmt"
)
func main() {
val := 0
for {
fmt.Print("Enter number: ")
fmt.Scanf("%d", &val)
switch {
case val < 0:
panic("You entered a negative number!")
case val == 0:
fmt.Println("0 is neither negative nor positive")
default:
fmt.Println("You entered:", val)
}
}
}
请记住,目的是练习无限循环和 switch 语句。
总结
目前,你已知道 Go 不同于其他编程语言之处。 例如,Go 不需要你在 if、for
或 switch
语句的条件中添加括号。 但是,你始终需要添加大括号 ({})
。 可以链接 if
语句,else
子句是可选的。 至关重要的是,可以在 if
条件中声明变量,其作用域仅限 if
块。 即使在同一函数中,也不能访问块外的这些变量。
Go 支持 switch
语句,你不需要编写条件。 只需使用 case
子句即可。 与其他语言不同的是,在 Go 中,无需在每个 case
子句末尾编写 break
语句来避免运行其他 case
子句。
默认情况下,Go 在进入 case
语句后就会运行它,然后退出 switch
子句。 若要跳转到下一个 case
子句,请使用 fallthrough
关键字。 可以从 case
子句调用函数,并且可以在一个 case
子句中将多个表达式分组。
在此模块中,你还了解到,在 Go 中只使用 for
关键字来编写循环。 但是,你可以编写无限循环或 while
条件。 Go 支持 continue
关键字,因此你可以在不退出循环的情况下跳过循环迭代。
最后,你了解了其他 Go 控制流,例如 defer
、panic
和 recover
函数。 Go 不支持异常。 它通过使用这三个函数的组合来处理运行时错误。
本文转自:SDK社区(sdk.cn)是一个中立的社区,这里有多样的前端知识,有丰富的api,有爱学习的人工智能开发者,有风趣幽默的开发者带你学python,还有未来火热的鸿蒙,当各种元素组合在一起,让我们一起脑洞大开共同打造专业、好玩、有价值的开发者社区,帮助开发者实现自我价值!