golang学习笔记2——函数

本文详细介绍了Go语言中的函数定义、参数传递、匿名函数与闭包的概念,以及错误处理的defer语句和error类型。此外,还探讨了Go的异常处理机制panic和recover的使用,展示了如何在遇到错误时优雅地处理问题。
摘要由CSDN通过智能技术生成

1. go函数

1.1 如何定义一个函数

go 定义一个函数比较简单,由关键字、函数名、参数类型、返回类型 组成,语法如下:

// optionalParameters 是 (param1 type1, param2 type2 ...) 这种形式
func functionName(optionalParameters) optionalReturnType {
  body
}

来看一个非常简单的函数,计算两个数字之和:

func sum0(a int, b int) int {
    return a + b
}
// 当多个参数类型相同时,可以只写一个类型声明
func sum1(a, b int) int {
    return a + b
}
// 可以给返回值命名,并且通过赋值当方式更新结果,而且return可以不带返回值
func sum2(a, b int) (res int) {
    res = a + b
    return
}

go 还支持可变参数,在 python 里我们知道使用的是 *args,在 go 里边使用三个省略号来实现, 比如想要计算 n 个 int 数字之和,可以这么写:(注意可变参数其实被包装成了一个 slice)

func sum3(init int, vals ...int) int {
    sum := init
    for _, val := range vals { // vals is []int
        sum += val
    }
    return sum
}
// fmt.Println(sum3(0, 1, 2, 3))
// fmt.Println(sum3(0, []int{1,2,3}...))  // 还可以解包一个 slice 来作为参数传入,给一个 slice 加上三个省略号

再进一步,函数还可以返回多个值,这个相比 c 来说非常方便,比如除了 sum 之外我们再返回一个可变参数的个数: (其实 go 最后一个参数经常用来返回错误类型,这个之后讨论错误处理的时候再涉及)

func sum4(init int, vals ...int) (int, int) {
    sum := init
    fmt.Println(vals, len(vals))
    for _, val := range vals {
        sum += val
    }
    return sum, len(vals)
}

1.2 函数的参数

Go针对不同的数据类型,在作为函数参数的时候,会有不同的效果

  • 内置类型:数值类型、字符串、布尔类型、数组。传递的是副本 (所以一般不用数组啦),会拷贝原始值,无法修改
  • 引用类型: 切片slice、映射map、通道、接口和函数类型。通过复制传递应用类型值的副本,本质上就是共享底层数据结构。可以修改

实际上,这里其实 map/slice 等也是传递的副本,为啥它们就可以修改呢?我们以 slice 举例,它的内部实现其实是这样的,底层实现包含一个指向数组的指针(ptr), 一个长度 len 和容量 cap ,传参的时候实际上是 slice 这个结构体的拷贝(只有三个元素而不是copy所有的底层数组里的值),所以复制很轻量,而且通过底层的指针也可以实现修改。

// https://golang.org/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

所以我们看到go 里边所有的函数参数都是值拷贝,只不过对于一些复合结构因为复制的结构体里包含指针,所以可以修改它的底层结构。

1.3 匿名函数与闭包

所谓闭包就是一个函数“捕获”了和它在同一作用域的其他常量和变量。 当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量,并且只要闭包还在使用它,这些变量就不会销毁,一直存在。 事实上,闭包能够捕获变量的原因,是把变量移动到了堆区,这样就能够保证程序结束之前,能够一直调用。

因此,go程序中,变量的生命周期不由它的作用域决定。

package main

import (
	"fmt"
	"strconv"
)
func tt() func() int {
	var x int
	fmt.Println("--------")
	fmt.Println(x)
	return func() int {
		x++
		return x * x
	}
}
func main() {

	i := 0
	str := "mike"

	f1 := func() { // 匿名函数,无参无返回值
		// 可以引用到函数外的变量,此时就是闭包. 并且函数内部可以改变函数引用变量的值
		i++
		str += strconv.Itoa(i) + " "
		fmt.Printf("方式1: i = %d, str = %s\n", i, str)
	} // 只定义了,还未调用

	f1() // 显示调用
	fmt.Printf("打印: i = %d, str = %s\n", i, str)

	// 方式1的另一种方式
	type FuncType func()
	var f2 FuncType = f1
	fmt.Println("f2()")
	f2() // 调用函数
	fmt.Printf("打印: i = %d, str = %s\n", i, str)

	//方式2
	func() { // 匿名函数 无参无返回
		i++
		str += strconv.Itoa(i)
		fmt.Printf("方式2: i = %d, str = %s\n", i, str)
	}() // ()的作用是,此处直接调用匿名函数

	// 方式3,有参有返回
	v := func(a, b int) (res int) {
		res = a + b
		return
	}

	v1 := v(1, 2) // 此处是调用函数
	fmt.Println("v1 =", v1)
	v2 := v // 此处是函数定义赋值
	fmt.Println("v2(3, 4) :", v2(3, 4))

	// 此处构成闭包,为什么?
	// 因为f 是一个 函数, 该函数由tt的返回值定义,且是一个匿名函数,因此就构成了闭包
	// 捕获了 tt 中的 x值, 但是捕获的 x 值, 捕获的是第一次调用tt()时创建的值,将其放入堆中
	f := tt()
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())

	// 以下就不会构成闭包,因为调用的是tt函数
	fmt.Println(tt()())
	fmt.Println(tt()())
	fmt.Println(tt()())

}

运行:

zhou@zhoudeMacBook-Air helloworld % go run 11_匿名函数.go
方式1: i = 1, str = mike1 
打印: i = 1, str = mike1 
f2()
方式1: i = 2, str = mike1 2 
打印: i = 2, str = mike1 2 
方式2: i = 3, str = mike1 2 3
v1 = 3
v2(3, 4) : 7
--------
0
1
4
9
16
--------
0
1
--------
0
1
--------
0
1
zhou@zhoudeMacBook-Air helloworld % 

1.4 函数类型

通过上一个例子可以知道,函数能够类似于“变量”一样的去赋值。实际上,go 里边函数其实也是『一等公民』,函数本身也是一种类型,所以我们可以定义一个函数然后赋值给一个变量,比如:

func testFuncType() {
    myPrint := func(s string) { fmt.Println(s) }
    myPrint("hello go")
}

这样的话,就可以用数据结构存储函数,比如用map值做函数的映射:

func testMapFunc() {
    funcMap := map[string]func(int, int) int{
        "add": func(a, b int) int { return a + b },
        "sub": func(a, b int) int { return a - b },
    }
    fmt.Println(funcMap["add"](3, 2))
    fmt.Println(funcMap["sub"](3, 2))
}

所以,也可以用作函数的参数传递进入,该方法一般可以用作回调函数:

func Double(n int) int {
    return n * 2
}
func Apply(n int, f func(int) int) int {
    return f(n) // f 的类型是 "func(int) int"
}
func funcAsParam() {
    fmt.Println(Apply(10, Double))
}

2. 错误处理

2.1 defer语句

go 中提供了一个 defer 语句用来延迟一个函数(匿名函数)或者方法的执行,它会在函数执行完成(return)之前调用。一般为了防止代码里有资源泄露(文件、数据库连接、锁), 对于打开的资源比如文件等我们需要显式关闭,这种场合就是 defer 发挥作用最好的场景,也是 go 代码中使用 defer 最常用的场景。

// go
f, err := os.Open(file)
if err != nil {
  // handle err
  return err
}
defer f.Close() // 保证文件会在函数返回之后关闭,防止资源泄露

// 也常用在使用锁的地方,防止忘记释放锁
mu.Lock()
defer mu.UnLock()

另外函数里可以使用多个 defer 语句,如果有多个 defer 它们会按照后进先出(Last In First Out)的顺序执行。

package main

import (
    "fmt"
)

func testDefer() string {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    fmt.Println("函数体")
    return "hello"
}

func main() {
    fmt.Println(testDefer())
}
// 输出;
/*
zhou@zhoudeMacBook-Air helloworld % go run 12_defer.go
函数体
defer 2
defer 1
hello
zhou@zhoudeMacBook-Air helloworld % 
*/

2.2 go 的 error 类型

在 go 中通过返回一个 error 来表示错误或者异常状态,这是 go 代码中最常见的方式。那 error 究竟是什么呢? 其实 error 是 go 的一个内置的接口类型,比如你可以使用开发工具跳进去看下 error 的定义。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

error 的定义很简单,只要我们自己实现了一个类型的 Error() 方法返回一个字符串,就可以当做错误类型了。举一个简单小例子, 比如计算两个整数相除,我们知道除数是不能为 0 的,这个时候我们就可以写个函数:

import (
    "errors" // 使用内置的 errors
    "fmt"
)

// Divide compute int a/b
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}

func main() {
    // fmt.Println(testDefer())
    a, b := 1, 0
    res, err := Divide(a, b)
    if err != nil {
        fmt.Println(err) // error 类型实现了 Error() 方法可以打印出来
    }
    fmt.Println(res)
}

2.3 错误处理

在 go 中使用的是类似 c 的返回错误的方式,比如我们在 go 代码中经常会看到很多这种错误检查代码。也就是通过error的值,来判断是否出现错误,比如:

// from https://8thlight.com/blog/kyle-krull/2018/08/13/exploring-error-handling-patterns-in-go.html
func (router HttpRouter) parse(reader *bufio.Reader) (Request, Response) {
  requestText, err := readCRLFLine(reader) //string, err Response
  if err != nil {
    //No input, or it doesn't end in CRLF
    return nil, err
  }

  requestLine, err := parseRequestLine(requestText) //RequestLine, err Response
  if err != nil {
    //Not a well-formed HTTP request line with {method, target, version}
    return nil, err
  }

  if request := router.routeRequest(requestLine); request != nil {
    //Well-formed, executable Request to a known route
    return request, nil
  }

  //Valid request, but no route to handle it
  return nil, requestLine.NotImplemented()
}

在 go 的惯例中,一般函数多个返回值的最后一个值用来返回错误,返回 nil 表示没有错误,调用者通过检查返回的错误是否是 nil 就知道是否需要处理错误了。

2.4 go 的异常处理 panic/recover

一般是使用error来提醒程序发生了错误,如果说想在出错时,立即终止程序,那么就使用panic来终止进程。比如刚才除法函数的例子,如果我们碰到了个除数为 0 被认为是严重错误,也可以使用 panic 抛出异常:

func MustDivide(a, b int) int {
    if b == 0 {
        panic("divide by zero")
    }
    return a / b
}

如果我们不幸传入了除数为0,但是又不想让进程退出呢?go 还提供了一个 recover 函数用来从异常中恢复,比如使用 recover 可以把一个 panic 包装成为 error 再返回,而不是让进程退出:

func Divide2(a, b int) (res int, e error) {
    defer func() {
        if err := recover(); err != nil {
            e = fmt.Errorf("%v", err)
        }
    }()
    res = MustDivide(a, b)
    return // 命名返回值不用加上返回的参数
}

此时就用error捕获了panic 异常并且返回了一个错误,代码也可以正常执行而不会退出啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值