关于golang:在-Go-中使用控制流

9次阅读

共计 11929 个字符,预计需要花费 30 分钟才能阅读完成。

简介

在上一模块中,你已理解 Go 的基础知识。你摸索了数据类型、变量、常量、函数和包。你晓得如何构建程序的逻辑,以及 Go 如何首选对代码进行组织。因而,当初你已筹备好学习如何应用 if/elseswitchesfor 之类的根本控制流和另一组关键字以 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 循环相似于 CJavaC# 之类的编程语言中的 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 中一些不太罕用的控制流:deferpanicrecover。如你所见,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() 函数时,能够增加任何值作为参数。通常,你会发送一条谬误音讯,阐明为什么会进入紧急状态。

例如,将 panicdefer 函数组合起来,以理解控制流中断的形式。然而,请持续运行任何清理过程。请应用以下代码片段:

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。你能够在此处执行一些操作来清理凌乱,但在这种状况下,你能够间接输入一些内容。

panicrecover 的组合是 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、forswitch 语句的条件中增加括号。然而,你始终须要增加大括号 ({})。能够链接 if 语句,else 子句是可选的。至关重要的是,能够在 if 条件中申明变量,其作用域仅限 if 块。即便在同一函数中,也不能拜访块外的这些变量。

Go 反对 switch 语句,你不须要编写条件。只需应用 case 子句即可。与其余语言不同的是,在 Go 中,无需在每个 case 子句开端编写 break 语句来防止运行其余 case 子句。

默认状况下,Go 在进入 case 语句后就会运行它,而后退出 switch 子句。若要跳转到下一个 case 子句,请应用 fallthrough 关键字。能够从 case 子句调用函数,并且能够在一个 case 子句中将多个表达式分组。

在此模块中,你还理解到,在 Go 中只应用 for 关键字来编写循环。然而,你能够编写有限循环或 while 条件。Go 反对 continue 关键字,因而你能够在不退出循环的状况下跳过循环迭代。

最初,你理解了其余 Go 控制流,例如 deferpanicrecover 函数。Go 不反对异样。它通过应用这三个函数的组合来解决运行时谬误。

本文转自:SDK 社区(sdk.cn)是一个中立的社区,这里有多样的前端常识,有丰盛的 api,有爱学习的人工智能开发者,有有趣风趣的开发者带你学 python,还有将来炽热的鸿蒙,当各种元素组合在一起,让咱们一起脑洞大开独特打造业余、好玩、有价值的开发者社区,帮忙开发者实现自我价值!

正文完
 0