Go学习 5、函数

5.1 函数声明

go 复制代码
func name(parameter-list) (result-list) {
    body
}
go 复制代码
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"
go 复制代码
func f(i, j, k int, s, t string)                 { /* ... */ }
func f(i int, j int, k int,  s string, t string) { /* ... */ }
go 复制代码
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
fmt.Printf("%T\n", add)  // "func(int, int) int"
fmt.Printf("%T\n", sub)  // "func(int, int) int"
fmt.Printf("%T\n", first) // "func(int, int) int"
fmt.Printf("%T\n", zero)  // "func(int, int) int"

函数的类型被称为函数的标识符。

没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义 了函数标识符。

go 复制代码
package math
func Sin(x float64) float //implemented in assembly language

5.2 递归

函数可以直接或间接的调用自身。

使用非标准包 golang.org/x/net/html ,解析HTML。

go 复制代码
package html
type Node struct {
    Type                    NodeType
    Data                    string
    Attr                    []Attribute
    FirstChild, NextSibling *Node
}
type NodeType int32
const (
    ErrorNode NodeType = iota
    TextNode//文本
    DocumentNode
    ElementNode
    CommentNode//注释
    DoctypeNode
)
type Attribute struct {
    Key, Val string
}
//读入一组bytes.解析 后,返回html.node类型的HTML页面树状结构根节点。
func Parse(r io.Reader) (*Node, error)

main函数解析HTML标准输入,通过递归函数visit获得links(链接),并打印出这些links:

go 复制代码
package main
import (
    "fmt"
    "os"
    "golang.org/x/net/html"
)

func main() {
    doc, err := html.Parse(os.Stdin)
    if err != nil {
        fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
		os.Exit(1) 
	}
    for _, link := range visit(nil, doc) {
        fmt.Println(link)
	} 
}

visit函数遍历HTML的节点树,从每一个anchor元素的href属性获得link,将这些links存入字符 串数组中,并返回这个字符串数组。

go 复制代码
// visit appends to links each link found in n and returns the result.
func visit(links []string, n *html.Node) []string {
    if n.Type == html.ElementNode && n.Data == "a" {
        for _, a := range n.Attr {
            if a.Key == "href" {
                links = append(links, a.Val)
            }
		} 
	}
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        links = visit(links, c)
	}
    return links
}

为了遍历结点n的所有后代结点,每次遇到n的孩子结点时,visit递归的调用自身。这些孩子结 点存放在FirstChild链表中。

在函数outline中,我们通过递归的方式遍历整个HTML结点树,并输出树的结构。在outline内 部,每遇到一个HTML元素标签,就将其入栈,并输出。

go 复制代码
func main() {
    doc, err := html.Parse(os.Stdin)
    if err != nil {
        fmt.Fprintf(os.Stderr, "outline: %v\n", err)
		os.Exit(1) 
	}
    outline(nil, doc)
}

func outline(stack []string, n *html.Node) {
    if n.Type == html.ElementNode {
        stack = append(stack, n.Data) // push tag
        fmt.Println(stack)
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        outline(stack, c)
	} 
}

大部分HTML页面只需几层递归就能被处理,但仍然有些页面需要 深层次的递归。

Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归 时不必考虑溢出和安全问题。

5.3 多返回值

go 复制代码
log.Println(findLinks(url))
links, err := findLinks(url)
log.Println(links, err)

准确的变量名可以传达函数返回值的含义

go 复制代码
func Size(rect image.Rectangle) (width, height int)
func Split(path string) (dir, file string)
func HourMinSec(t time.Time) (hour, minute, second int)

如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。

go 复制代码
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
}
func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

5.4 错误

panic是来自被调函数的信号,表示发生了某个已知的bug。

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一 个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。

go 复制代码
value, ok := cache.Lookup(key)
if !ok {
    // ...cache[key] does not exist...
}

导致失败的原因不止一种,额外的返回值不再是简单的布尔类型,而是error类型。error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。

go 复制代码
fmt.Println(err)
fmt.Printf("%v", err)

当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。

当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。

函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常。

Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。

5.4.1 错误处理策略

  • 传播错误。函数中某个子程序的失败,会变成该函数的失败。
go 复制代码
resp, err := http.Get(url)
if err != nil{
    return nil, err
}

构造新的错误信息返回给调用者:

go 复制代码
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v", url,err)
}

错误信息经常是以链式组合在一起的,错误信息中应避免大写和换行符。grep的工具处理错误信息

  • 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
go 复制代码
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 // success
        }
        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)
}
  • 如果错误发生后,程序无法继续运行,输出错误信息并结束程序。
go 复制代码
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

调用log.Fatalf:log中的所有函数,都默认会在错误信息之前输出时间信息。

go 复制代码
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

我们可以设置log的前缀信息屏蔽时间信息,一般而言,前缀信息会被设置成命令名。

go 复制代码
log.SetPrefix("wait: ")
log.SetFlags(0)
  • 只需要输出错误信息就足够了,不需要中断程序的运行

通过log包提供函数:log包中的所有函数会为没有换行符的字符串增加换行符。

go 复制代码
if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled",err)
}

标准错误流输出错误信息:

go 复制代码
if err := Ping(); err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
  • 直接忽略掉错误
go 复制代码
dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v",err)
}
// ...use temp dir...
os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically

5.4.2 文件结尾错误(EOF)

io包保证任何由文件结束引起的读取失败都返回同一个错误------io.EOF,该错误在io包中定义:

go 复制代码
package io
import "errors"
// EOF is the error returned by Read when no more input is available.
var EOF = errors.New("EOF")

如何从标准输入中读取字符,以及判断文件结束。

go 复制代码
in := bufio.NewReader(os.Stdin)
for {
    r, _, err := in.ReadRune()
    if err == io.EOF {
        break // finished reading
    }
    if err != nil {
        return fmt.Errorf("read failed:%v", err)
	}
// ...use r...
}

文件结束这种错误不需要更多的描述,所以io.EOF有固定的错误信息------"EOF"。

5.5 函数值

在Go中,函数被看作第一类值(first-class values)::函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。

go 复制代码
func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }
f := square
fmt.Println(f(3)) // "9"
f = negative
fmt.Println(f(3))     // "-3"
fmt.Printf("%T\n", f) // "func(int) int"
f = product // compile error: can't assign func(int, int) int to func(int) int

函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

go 复制代码
var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误

函数值可以与nil比较:

go 复制代码
var f func(int) int
if f != nil {
	f(3) 
}

但是函数值之间是不可比较的,也不能用函数值作为map的key。

strings.Map对字符串中的每个字符调用add1 函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。

go 复制代码
func add1(r rune) rune { return r + 1 }
fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
fmt.Println(strings.Map(add1, "VMS"))      // "WNT"
fmt.Println(strings.Map(add1, "Admix"))    // "Benjy"

5.6 匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。

函数值字面量是一种表达式,它的值被成为匿名函数 (anonymous function)。允许我们在使用函数时,再定义它。

go 复制代码
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")

可以访问完整的词法环境(lexical environment), 这意味着在函数中定义的内部函数可以引用该函数的变量

go 复制代码
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。 func squares() func() int {
    var x int
    return func() int {
		x++
		return x * x 
	}
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}

函数值属于引用类型和函数值不可比较

闭包(closures)技术、把函数值叫做闭包。

变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

当匿名函数需要被递归调用时,我们必须首先声明一个变量。再将匿名函数赋值给这个变量。

5.6.1警告:捕获迭代变量

你被要求首先创建一些目录,再将目录删除

go 复制代码
var rmdirs []func()
for _, d := range tempDirs() {
    dir := d // NOTE: necessary!
    os.MkdirAll(dir, 0755) // creates parent directories too
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}
// ...do some work...
for _, rmdir := range rmdirs {
    rmdir() // clean up
}

为什么要在循环体中用循环变量d赋值一个新的局部变量
这不是go或 defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。

5.7 可变参数

参数数量可变的函数称为为可变参数函数。

Printf首先接收一个的必备参数,之后接收任意个数的后续参数。

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号"...",这表示 该函数会接收任意数量的该类型参数。

go 复制代码
func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
	}
    return total
}

sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接 收任意数量的int型参数:

go 复制代码
fmt.Println(sum())           // "0"
fmt.Println(sum(3))          // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一 个切片作为参数传给被调函数。

如果原始参数已经是切片类型,我们该如何传递给sum?只需 在最后一个参数后加上省略符。

go 复制代码
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函 数和以切片作为参数的函数是不同的。

go 复制代码
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

可变参数函数经常被用于格式化字符串。

errorf函数构造了一个以行号开头的,经过格 式化的错误信息。

go 复制代码
func errorf(linenum int, format string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, "Line %d: ", linenum)
    fmt.Fprintf(os.Stderr, format, args...)
    fmt.Fprintln(os.Stderr)
}
linenum, name := 12, "count"
errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count"

interfac{}表示函数的最后一个参数可以接收任意类型

5.8 Deferred函数

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语 句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时, defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic 导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通 过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的 defer应该直接跟在请求资源的语句后。

文件操作:

go 复制代码
package ioutil
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return ReadAll(f)
}

互斥锁:

go 复制代码
var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
    mu.Lock()
    defer mu.Unlock()
    return m[key]
}

调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。

go 复制代码
func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the
    extra parentheses
    // ...lots of work...
    time.Sleep(10 * time.Second) // simulate slow
    operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() {
        log.Printf("exit %s (%s)", msg,time.Since(start))
    }
}

不要忘记defer语句后的圆括号,否则本该在进入时执行的操作 会在退出时执行,而本该在退出时执行的,永远不会被执行。

对匿名函数采用defer机 制,可以使其观察函数的返回值。

double函数:

go 复制代码
func double(x int) int {
    return x + x
}

首先命名double的返回值,再增加defer语句,我们就可以在double每次被调用时,输出参数以及返回值。

go 复制代码
func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"

对于有许多return语句的函数而言, 这个技巧很有用。

在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。

5.9 Panic异常

运行时错误会引起painc异常

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信 息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。

由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。

go 复制代码
func main() {
    f(3)
}
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)
}
go 复制代码
f(3)
f(2)
f(1)
defer 1
defer 2
defer 3

当f(0)被调用时,发生panic异常,之前被延迟执行的的3个fmt.Printf被调用。程序中断执行后,panic信息和堆栈信息会被输出。

runtime包允许程序员输出堆栈信息

在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。

5.10 Recover捕获异常

从异常中恢复

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选 择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

go 复制代码
func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
		} 
	}()
    // ...parser...
}

deferred函数帮助Parse从panic中恢复。在deferred函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。我们也可以通过调用runtime.Stack往错误信 息中添加完整的堆栈调用信息。

不加区分的恢复所有的panic异常,不是可取的做法

不应该试图去恢复其他包引起的panic

有选择性的recover,只恢复应该被恢复的panic异常。这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。

相关推荐
问道飞鱼10 分钟前
每日学习一个数据结构-B+树
数据结构·b树·学习
尘浮生19 分钟前
Java项目实战II基于Java+Spring Boot+MySQL的校园社团信息管理系统(源码+数据库+文档)
java·开发语言·数据库·spring boot·mysql·spring·maven
不染_是非40 分钟前
Django学习实战篇六(适合略有基础的新手小白学习)(从0开发项目)
后端·python·学习·django
qq_18735263442 分钟前
马踏棋盘c++
开发语言·c++·马踏棋盘c++
巭犇1 小时前
c语言中define使用方法
c语言·开发语言
Midsummer啦啦啦1 小时前
NumPy库学习之argmax函数
学习·numpy
Mero技术博客1 小时前
第二十节:学习Redis缓存数据库实现增删改查(自学Spring boot 3.x的第五天)
数据库·学习·缓存
qing_0406031 小时前
C++——string的了解和使用
开发语言·c++·string
QuantumYou1 小时前
【对比学习串烧】 SWav和 BYOL
学习·机器学习
The Straggling Crow1 小时前
go 战略
开发语言·后端·golang