Go学习 5、函数

5.1 函数声明

func name(parameter-list) (result-list) {
    body
}
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"
func f(i, j, k int, s, t string)                 { /* ... */ }
func f(i int, j int, k int,  s string, t string) { /* ... */ }
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实现的。这样的声明定义 了函数标识符。

package math
func Sin(x float64) float //implemented in assembly language

5.2 递归

函数可以直接或间接的调用自身。
使用非标准包 golang.org/x/net/html ,解析HTML。

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:

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存入字符 串数组中,并返回这个字符串数组。

// 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元素标签,就将其入栈,并输出。

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 多返回值

log.Println(findLinks(url))
links, err := findLinks(url)
log.Println(links, err)

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

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语句可以省略操作数。

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。

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函数或者输出函数获得字符串类型的错误信息。

fmt.Println(err)
fmt.Printf("%v", err)

当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。
当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。
函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常。
Go使用控制流机制(如if和return)处理异常,这使得编码人员能更多的关注错误处理。

5.4.1 错误处理策略

  • 传播错误。函数中某个子程序的失败,会变成该函数的失败。
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)
}

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

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

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

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

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

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

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

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

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

if err := Ping(); err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
  • 直接忽略掉错误
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包中定义:

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

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

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)的调用类似函数调用。

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错误:

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

函数值可以与nil比较:

var f func(int) int
if f != nil {
	f(3) 
}

但是函数值之间是不可比较的,也不能用函数值作为map的key。
strings.Map对字符串中的每个字符调用add1 函数,并将每个add1函数的返回值组成一个新的字符串返回给调用者。

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)。允许我们在使用函数时,再定义它。

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

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

// 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警告:捕获迭代变量

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

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首先接收一个的必备参数,之后接收任意个数的后续参数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示 该函数会接收任意数量的该类型参数。

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

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

fmt.Println(sum())           // "0"
fmt.Println(sum(3))          // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一 个切片作为参数传给被调函数。
如果原始参数已经是切片类型,我们该如何传递给sum?只需 在最后一个参数后加上省略符。

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

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

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

可变参数函数经常被用于格式化字符串。
errorf函数构造了一个以行号开头的,经过格 式化的错误信息。

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应该直接跟在请求资源的语句后。
文件操作:

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)
}

互斥锁:

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

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

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函数:

func double(x int) int {
    return x + x
}

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

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一般用于严重错误,如程序内部的逻辑不一致。

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)
}
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异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

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设置成特殊类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值