Go中流程控制和函数

2023年 8月 22日 62.1k 0

大家好,最近在系统写Go方面入门文章,如果感兴趣的同学可以点点关注,如果遇到相关方面的问题也可以评论,或者是私聊。你的关注是作者持续更新下去的动力。 本文较为基础,内容来源已经标注,如果侵权请及时联系删除。

流程控制

Go 中流程控制分三大类:条件判断,循环控制和无条件跳转。

if

if 如果满足条件就执行某段逻辑,否则执行另一个逻辑。 Go 里面 if 条件判断语句中不需要括号,如下:

if x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

Go 的 if 还有一个特殊的地方是条件判断语句里面允许声明一个变量,这个变量作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示

// 计算获取值 x, 然后根据 x 返回的大小,判断是否大于 10。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

// 这个地方如果这样调用就编译出错了,因为 x 是条件里面的变量
fmt.Println(x)

goto

Gogoto 语句 —— 请明智地使用它。用 goto 跳转到必须在当前函数内定义的标签。例如假设这样一个循环:

func myFunc() {
    i := 0
Here:   
    println(i)
    i++
    goto Here   // 跳转到 Here 去
}

标签名大小写敏感

for

Go 里面最强大的一个控制逻辑就是 for,它既可以用来循环读取数据,又可以当作 while 来控制逻辑,还能迭代操作。它的语法如下:

for expression1; expression2; expression3 {
    //...
}

expression1expression2expression3 都是表达式,其中 expression1expression3 是变量声明或者函数调用返回值之类的,expression2 是用来条件判断,expression1 在循环开始之前调用,expression3 在每轮循环结束之时调用。

例如下面的代码:

package main

import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45

其中 ; 也可以省略,那么就变成如下的代码了,这就是 while 的功能。

sum := 1
for sum < 1000 {
    sum += sum
}

在循环里面有两个关键操作 breakcontinue, break 操作是跳出当前循环,continue 是跳过本次循环。

当嵌套过深的时候, break 可以配合标签使用,即跳转至标签所指定的位置.

breakcontinue 还可以跟着标号,用来跳到多重循环中的外层循环.

for 配合 range 可以用于读取 slicemap 的数据:

for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

switch

有些时候你需要写很多的 if-else 来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候 switch 就能很好的解决这个问题。它的语法如下

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

Goswitch 非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;

而如果 switch 没有表达式,它会匹配 true

同样的 Go 中支持多个变量写在同一行中,Go 里面 switch 默认相当于每个 case 最后带有 break,匹配成功后不会自动向下执行其他 case,而是跳出整个 switch, 但是可以使用 fallthrough 强制执行后面的 case 代码。

函数

函数是 Go 里面的核心设计,它通过关键字 func 来声明,它的格式如下:

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    // 这里是处理逻辑代码
    // 返回多个值
    return value1, value2
}

函数定义有如下特征:

  • 关键字 func 用来声明一个函数 funcName
  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过 , 分隔
  • 函数可以返回多个值
  • 上面返回值声明了两个变量 output1 和 output2,如果你不想声明也可以,直接就两个类型
  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
  • 如果没有返回值,那么就直接省略最后的返回信息
  • 如果有返回值, 那么必须在函数的外层添加 return 语句

多个返回值

Go 语言比 C 更先进的特性,其中一点就是函数能够返回多个值。

package main

import "fmt"

// 返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}

func main() {
    x := 3
    y := 4

    xPLUSy, xTIMESy := SumAndProduct(x, y)

    fmt.Printf("%d + %d = %dn", x, y, xPLUSy)
    fmt.Printf("%d * %d = %dn", x, y, xTIMESy)
}

上面例子可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。

但如果你的函数是导出的 (首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。

下面是定义返回值名称,定义后就可以直接在代码中使用此变量

// 返回 A+B 和 A*B
func SumAndProduct(A, B int) (Add int, Multiplied int) {
    Add = A+B
    Multiplied = A*B
    return
}

变参

Go 函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:

func myfunc(arg ...int) {}

arg ...int 告诉 Go 这个函数接受不定数量的参数。注意,这些参数的类型全部是 int。在函数体中,变量 arg 是一个 intslice

for _, n := range arg {
    fmt.Printf("And the number is: %dn", n)
}

传值与传指针

当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份 copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在 copy 上。

package main

import "fmt"

// 简单的一个函数,实现了参数+1的操作
func add1(a int) int {
    a = a+1 // 我们改变了 a 的值
    return a // 返回一个新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(x)  // 调用 add1(x)

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 3"
}

看到了吗?虽然我们调用了 add1 函数,并且在 add1 中执行 a = a+1 操作,但是上面例子中 x 变量的值没有发生变化。

理由很简单:因为当我们调用 add1 的时候,add1 接收的参数其实是 x 的 copy,而不是 x 本身。

那你也许会问了,如果真的需要传这个 x 本身,该怎么办呢?

这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有 add1 函数知道 x 变量所在的地址,才能修改 x 变量的值。所以我们需要将 x 所在地址 &x 传入函数,并将函数的参数的类型由 int 改为 *int,即改为指针类型,才能在函数中修改 x 变量的值。此时参数仍然是按 copy 传递的,只是 copy 的是一个指针。

请看下面的例子

package main

import "fmt"

// 简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

看到了吗?虽然我们调用了 add1 函数,并且在 add1 中执行 a = a+1 操作,但是上面例子中 x 变量的值没有发生变化

理由很简单:因为当我们调用 add1 的时候,add1 接收的参数其实是 x 的 copy,而不是 x 本身。

那你也许会问了,如果真的需要传这个 x 本身,该怎么办呢?

这就牵扯到了所谓的指针。

我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有 add1 函数知道 x 变量所在的地址,才能修改 x 变量的值。所以我们需要将 x 所在地址 &x 传入函数,并将函数的参数的类型由 int 改为 *int,即改为指针类型,才能在函数中修改 x 变量的值。此时参数仍然是按 copy 传递的,只是 copy 的是一个指针。

package main

import "fmt"

// 简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

这样,通过指针形式也达到了修改 x 的目的。那么到底传指针有什么好处呢?

传指针使得多个函数能操作同一个对象。

传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话,在每次 copy 上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。

Go 语言中 channelslicemap 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变 slice 的长度,则仍需要取地址传递指针)

defer

Go 语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个 defer 语句。

当函数执行到最后时,这些 defer 语句会按照逆序执行,最后该函数返回。

特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

此种设计方法极大降低了资源泄漏问题,尤其是一些文本句柄打开后忘记关闭,进而导致内存泄漏问题。并且代码量极大减少了,代码也变的更加优雅。

defer是一个栈设计模式,采用后进先出模式,

函数作为值、类型

Go 中函数也是一种变量,我们可以通过 type 来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子。
函数作为一等公民出现,让代码的组织形式更加灵活。


package main

import "fmt"

type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// 声明的函数类型在这个地方当做了一个参数

func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 函数当做值来传递了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 函数当做值来传递了
    fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在实现一些通用接口的时候非常有用,通过上面例子我们看到 testInt 这个类型是一个函数类型,然后两个 filter 函数的参数和返回值与 testInt 类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

Panic 和 Recover

Go 没有像其他语言那样的异常机制,它不能抛出异常,而是使用了 panicrecover 机制。

一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有 panic 的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?

Panic

是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。

当函数 F 调用 panic,函数 F 的执行被中断,但是 F 中的延迟函数会正常执行,然后 F 返回到调用它的地方。 在调用的地方,F 的行为就像调用了 panic。这一过程继续向上,直到发生 panicgoroutine 中所有调用的函数返回,此时程序退出。

恐慌可以直接调用 panic 产生。

也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。

recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

下面这个函数演示了如何在过程中使用 panic

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

下面这个函数检查作为其参数的函数在执行时是否会产生 panic

func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() // 执行函数f,如果f中出现了panic,那么就可以恢复回来
    return
}

main 函数和 init 函数

Go 里面有两个保留的函数:init 函数(能够应用于所有的 package )和 main 函数(只能应用于 package main)。

这两个函数在定义时不能有任何的参数和返回值。

虽然一个 package 里面可以写任意多个 init 函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个 package 中每个文件只写一个 init 函数。

Go 程序会自动调用 init()main(),所以你不需要在任何地方调用这两个函数。每个 package 中的 init 函数都是可选的,但 package main 就必须包含一个 main 函数。

程序的初始化和执行都起始于 main 包。

如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。 有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。

当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。下图详细地解释了整个执行过程:

main 函数引入包初始化流程图

关于他们的更多介绍,在上个章节中也有很详细介绍。

import

我们在写 Go 代码的时候经常用到 import 这个命令用来导入包文件。

上面这个 fmtGo 语言的标准库,其实是去 GOROOT 环境变量指定目录下去加载该模块,当然 Goimport 还支持如下两种方式来加载自己写的模块:

  • 相对路径

import "./model" 当前文件同一目录的 model 目录,但是不建议这种方式来 import

  • 绝对路径

import "shorturl/model" 加载 gopath/src/shorturl/model 模块

上面展示了一些 import 常用的几种方式,但是还有一些特殊的 import.

点操作

我们有时候会看到如下的方式导入包

import (
    . "fmt"
)

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的 fmt.Println ("hello world") 可以省略的写成 Println ("hello world")

别名操作

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

import(
    f "fmt"
)

别名操作的话调用包函数时前缀变成了我们的前缀,即 f.Println ("hello world")

_操作

这个操作经常是让很多人费解的一个操作符,请看下面这个 import

import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的 init 函数。

参考链接:

  • learnku.com/docs/build-…

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论