【go】gopl学习笔记(2.函数)

本文详细介绍了Go语言中的函数,包括函数声明、调用、返回值、命名返回值、错误处理机制、函数值、匿名函数和闭包、可变参数、Deferred函数调用、 Panic异常及捕获异常。重点讨论了Go的错误处理方式,强调了动态增长的栈对于递归的影响,以及错误信息的规范化。此外,还提到了函数值作为行为传递的灵活性和闭包的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.函数

1.1 函数声明

1.1.1 第三方类库

1.2 函数调用&递归

1.3 返回值

1.4.1 命名返回值

1.4 错误机制errors

1.4.1 传递错误

1.4.2 重试策略

1.4.3 终止执行&logerror

1.4.4 不终止不处理

1.5 函数值!!

1.6 匿名函数

1.6.1 闭包

1.7 可变参数

1.8 Deferred函数调用

1.9  Panic异常

1.10 捕获异常


1.函数

如下函数使用func标识,包含函数签名和函数体。

func name(parameter-list) (result-list) {
body
}
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}

1.1 函数声明

函数包含签名和函数体,函数体很好理解,就是语句的集合

签名包含func、函数名、参数列表、返回值,与其它语言类似,比较明显的区别有两个

  1. 参数列表的参数名、参数类型的声明顺序,且相同类型可以简写
  2. 返回值是个列表,长度是[0,n],可以没有返回值(类似java void),也可有多个返回值;可以命名如下z
  3. 函数传参是值传递,传入拷贝的值,只有指针, slice, map, function, channel这5个引用类型修改在函数外有效,其余都无效
func add(x int, y int) int { return x + y }
func sub(x, y int) (z int) { z = x - y; return }
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }

1.1.1 第三方类库

函数就是为了复用,go团队在golang.org/x/仓库中提供了大量的非标准,为什么不将它们放在标准库中呢?有的因为还在开发阶段,有的因为只有少部分人需要引用。

go get等获取golang.org/x/下的包一般会报错,因为外网防火墙的原因,所以一般从对应的github仓库将其下载到本地${GOPATH}/src/golang.org/x下,类似于maven的远程仓库和本地仓库,如下命令

# golang.org
git clone git@github.com:golang/net.git ${GOPATH}/src/golang.org/x/net

1.2 函数调用&递归

与大多数语言语法类似,如下,html是引入的包,Parse是export导出的方法,参数是标准输入,返回值赋值给doc和err。

doc, err := html.Parse(os.Stdin)

与大多数语言类似,支持递归,递归就是直接或者间接地调用自身,使用场景如树的深度遍历等等。

与大多数语言递归不同的时,很多语言如Java有栈大小(64KB-2MB)的限制,递归深度过大可能抛出栈溢出的异常,(通常是因为递归边界没有写好),但是Go的栈大小是可以动态增长地,一直到GB级别的大小。所以在递归时要更加注意边界,否则可能侵占更多资源,更长时间才能发现!!

1.3 返回值

Go可以有多个返回值,约定俗成的是一般函数调用返回结构和是否成功的标识符。如果不是以上的惯例需要通过函数命名传达返回值有几个、什么含义。

return result, nil  // return语句

resp, err := http.Get(url)  // 调用,返回值赋值
links, _ := findLinks(url) // errors ignored ,如果不需要某个返回值,需要将其赋值给_

1.4.1 命名返回值

如果返回值有了名字,则可以省略return语句后面的变量(裸return),因为其作为局部变量在函数被使用了。

func CountWordsAndImages(url string) (words, images int, err error) {
	resp, err := http.Get(url)
	if err != nil {
		return
	}
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		err = fmt.Errorf("parsing HTML: %s", err)
		return
	}
	words, images = countWordsAndImages(doc)
	return
}

但由于可读性不高,有歧义,最好谨慎使用裸return。

1.4 错误机制errors

函数分为两种,一种是确保能够正常执行,还有一种是不能确保能够正常执行(如读取问题,网络传输等等)。Go的异常处理(Error Handling),只有一种错误原因的像上文所述一样附带返回一个bool值表达是否成功即可,但大多数情况是有很多种失败的可能性,需要传递给用户错误码和原因,所以go内置了error类型(是个interface)如下,nil时表示功能,非nil时可以从中获取失败原因。

type error interface {
	Error() string
}

1.4.1 传递错误

与其它语言的异常处理(exception mechanism)不同,没有throw 和catch等流程控制的机器,异常处理还是通过if和return等普通指令来完成,如下,异常处理就是判断错误err,如果有错误将继续往上抛(传递),如果一直抛到main函数还不处理的话会处理异常退出。

resp, err := http.Get(url)
if err != nil {
  return nil, err  // 原样抛出
}

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) // 处理后抛出,包装异常error msg
}

 错误信息规范

error message不应该包含大写和换行符,这样使用grep进行搜索可以看到完整的错误链。

  1. 错误信息(error message)尽量详尽,同包风格保持一致
  2. 函数调用 函数体应传递函数、参数等上下文,调用处应补充信息

1.4.2 重试策略

如果错误是偶发,可以重试(限制重试的时间间隔或重试的次数)

func WaitForServer(url string) error {
	const timeout = 1 * time.Minute
	deadline := time.Now().Add(timeout)
	for tries := 0; time.Now().Before(deadline); tries++ { // 时间限制
		_, err := http.Head(url)
		if err == nil {
			return nil // 成功返回
		}
		log.Printf("server not responding (%s); retrying...", err)
		time.Sleep(time.Second << uint(tries)) // exponential back-off
	}
	return fmt.Errorf("server %s failed to respond after %s", url, timeout) // 传递错误
}

1.4.3 终止执行&logerror

类似于Java的RuntimeException,错误发生后,程序无法继续运行,则输出错误信息并结束程序。通常在main函数中进行,库函数应该往上抛出。
 

if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) // 标准错误
    os.Exit(1)
}
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err) // 日志
}

1.4.4 不终止不处理

类似Java中的很多受检查的非RuntimeException,catch住后可以不进行强制处理,打印相关日志或者什么都不做,然后继续执行。

1.5 函数值!!

函数像其他值一样,

  1. 拥有类型, 如func(rune) rune,展示了参数和返回值列表
  2. 可赋值给变量,
  3. 传递给函数,
  4. 从函数返回
  5. 可以被调用

函数类型的零值是nil,调用值为nil的函数值会引起panic错误,但是两个非nil的函数值不能==比较,也就不能作为map的key。

函数值的出现,类似于C的函数引用,可以作为参数传递给另一个函数进行调用,这样函数调用不仅仅可以传递数据,也可以传递行为。

标准类库strings.Map,声明和调用如下,对字符串中的每个字符调用add1函数,并将每个add1函数的返回值组成一个新串返回。

func Map(mapping func(rune) rune, s string) string {} // 第一个mapping函数,将第二个参数string每个字符进行映射

fmt.Println(strings.Map(add1, "Admix")) // "Benjy"

这种机制可以促进抽象,类似于模板模式,将不同的行为作为参数传入,相同的流程步骤放大模板方法,这样的设计更抽象、灵活。

1.6 匿名函数

拥有函数名的函数只能在包级语法块中被声明,为了绕过这一限制,type或者func内部可以使用函数字面量(匿名函数),类似于Java中的匿名类,如果不需要复用或者缩小其scope,可以使用匿名函数。于函数声明的区别就是没有函数名。

strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")

1.6.1 闭包

匿名函数有一个很有用的特性,内部的匿名函数可以访问其enclosing函数(外部函数)的变量;类似于Java的内部类,也有个指向外部类的引用,匿名函数也有引用指向外部函数变量,函数值不仅仅是一串代码,还记录了状态。这也是为什么函数值属于引用类型,为什么函数值不可比较的原因。

func squares() func() int {
	var x int
	return func() int {
		x++  // 闭包,可以访问其outer的变量 
		return x * x
	}
}

当匿名函数需要递归的时候怎么办呢??

  1. 定义函数类型变量
  2. 将匿名函数赋值给变量
  3. 匿名函数内部 调用函数值变量
	var visitAll func(items []string) // 函数值变量,不是函数名哦~

	visitAll = func(items []string) { // 匿名函数 DFS的方式
		for _, item := range items {
			if !seen[item] {  // 闭包访问
				seen[item] = true
				visitAll(m[item]) // 递归 最终将叶子先放入结果中
				order = append(order, item)
			}
		}
	}

需要注意的是,

  1. x已经不再是个单纯的局部变量,它的scope不一样,在外部函数squares return返回了以后,x还不会消失,因为f还引用这它。
  2. 匿名函数内部不要引用外部for的迭代变量,因为如下的匿名函数中引用的dir只记录了内存地址,所以是同一个,而不是每次迭代的值。
var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}
// 引入同名变量 
for _, dir := range tempDirs() {
    dir := dir // declares inner dir, initialized to outer dir
    // ...
}

1.7 可变参数

函数可以接收任意个数的参数,Java也有类似的机制,如Java的main函数,可以接收任意个程序参数。如下表示接收任意个int类型的参数。常用格式化输出函数就是典型,如下,interfac{}表示函数的最后一个参数可以接收任意类型。

func Printf(format string, a ...interface{}) (n int, err error) {}
func sum(vals...int) int {}
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10" 传入切片,后面加...

原理就是:隐式创建数组arr,拷贝原始参数,再把切片arr[:]作为参数传给被调函数。如果原始参数已经是切片类型,我们该如何传递给sum?只需在最后一个参数后加上省略符。

1.8 Deferred函数调用

为了保证资源释放和去除冗余,在Java中我们会在try-catch-finally的finally块中进行资源释放,Go也有类似的机制,就是defer。

如果没有defer机制的话,在代码执行的过程中,异常和正常逻辑 块中都需要写资源释放的代码,这样就会有很多冗余,也会有遗漏的情况,维护也很困难。

语法简单,只需在调用普通函数或方法前加上关键字defer,执行过程是 当前函数执行完毕(return)后才会执行defer后接的函数就像是try-catch包含了整个的函数代码,defer后面的函数就是finally块。

// IO资源
defer resp.Body.Close()

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return ReadAll(f)
}
// 互斥锁释放
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}

常用于资源清退,也用于函数trace等。

注意,如果需要在循环体中多次调用defer语句,应抽取新的子函数,否则不会迭代调用,仅在函数退出时调用一次许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈,所以需要关注colse的异常,如果没有检查文件关闭时的error,可能会导致数据丢失,所以写入文件后关闭文件时,我们不对f.close采用defer机制。

1.9  Panic异常

java在遇到throw了RuntimeException时,会执行outer的finally块,然后线程终止,一般退出时会打印异常堆栈信息到标准错误。Go的panic机制也是类似的,panic异常发生,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制),程序崩溃并输出日志信息,日志信息包括panic value和函数调用的堆栈跟踪信息

func main() {
	f(1)
}
func f(x int) {
	fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0
	defer fmt.Printf("defer %d\n", x)
	f(x - 1)
}

内置panic函数类似于Java的throw,可以抛出一个运行时异常,引起程序的崩溃。尽量少用panic,尽量使用上文1.4的错误机制。

switch s := suit(drawCard()); s {
    case "Spades":                                // ...
    case "Hearts":                                // ...
    case "Diamonds":                              // ...
    case "Clubs":                                 // ...
    default:
        panic(fmt.Sprintf("invalid suit %q", s)) // Joker? 类似于断言抛出运行时异常
}

1.10 捕获异常

既然有了类似于java的throw的panic,也得有类似于java的catch的recover来捕获异常。就像Java异常分为运行时异常的分类,程序中选择性地在执行过程中恢复或者终止。Go也是这样,只恢复应该被恢复的panic异常,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理。

	defer func() {
		switch p := recover(); p {
		case nil:
			// no panic
		case bailout{}:
			// "expected" panic
			err = fmt.Errorf("multiple title elements")
		default:
			panic(p) // unexpected panic; carry on panicking
		}
	}()

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值