05函数

         1:函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

func name(parameter-list) (result-list) {
    body
}

返回值列表描述了函数返回值的变量名以及类型,返回值也可以像形式参数一样被命名。在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。如果函数返回一个无名变量,返回值列表的括号是可以省略的。

 

         2:如果一组形参或返回值有相同的类型,不必为每个形参都写出参数类型。下面2个声明是等价的:

func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }

         3:函数的类型被称为函数的标识符。如果两个函数形参列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符:

func sub(x, y int) (z int) { z = x - y; return}
func main() {
   fmt.Printf("%T
", sub) // "func(int, int) int"
}

         4:每一次函数调用都必须按照声明顺序为所有参数提供实参。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参。

 

5:在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的简介引用被修改。

 

6:大部分编程语言使用固定大小的函数调用栈,固定大小栈会限制递归的深度,当用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。相反地,Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

 

7:在Go中,一个函数可以返回多个值。许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息。

调用多返回值函数时,调用者必须显式的将这些值分配给变量:

links, err := findLinks(url)。

如果某个值不被使用,可以将其分配给blank identifier:

links, _ := findLinks(url) // errors ignored

 

8:准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:

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

 

9:如果一个函数所有的返回值都有变量名,那么该函数的return语句可以省略操作数。这称之为bare 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语句等价于:return words, images, err

当一个函数有多处return语句以及许多返回值时,bare return 可以减少代码的重复,但是使得代码难以被理解。举个例子,如果你没有仔细的审查代码,很难发现前2处return等价于return 0,0,err,最后一处return等价于 return words,image,nil。基于以上原因,不宜过度使用bare return。

 

10:对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok。比如,cache.Lookup失败的唯一原因是key不存在,那么代码可以按照下面的方式组织:

value, ok := cache.Lookup(key)
if !ok {
    // ...cache[key] does not exist…
}

通常,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值可以是error类型。内置的error是接口类型。我们将在第七章了解接口类型的含义,以及它对错误处理的影响。现在我们只需要明白error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。对于non-nil的error类型,我们可以通过调用error的Error函数或者输出函数获得字符串类型的错误信息。

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

下面是常见的错误处理方式:

直接传播错误:

resp, err := http.Get(url)
if err != nil{
    return nill, err
}

添加错误定位调试信息:

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

读文件遇到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)
    }
}

11:在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
", 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。

 

12:匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),可以绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。也就是说Go不能在函数内部显式嵌套定义函数,但是可以定义一个匿名函数。比如:

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

通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量。如下例所示:

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"

    g := squares()
    fmt.Println(g()) // "1"
    fmt.Println(g()) // "4"
    fmt.Println(g()) // "9"
    fmt.Println(g()) // "16"
}

函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。

squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

 

考虑这样一个具体的例子:给定一些计算机课程,每个课程都有前置课程,只有完成了前置课程才可以开始当前课程的学习;我们的目标是选择出一组课程,这组课程必须确保按顺序学习时,能全部被完成。这本质上是一个拓扑排序问题:

var prereqs = map[string][]string{
    "algorithms": {"data structures"},
    "calculus": {"linear algebra"},
    "compilers": {
        "data structures",
        "formal languages",
        "computer organization",
    },
    "data structures": {"discrete math"},
    "databases": {"data structures"},
    "discrete math": {"intro to programming"},
    "formal languages": {"discrete math"},
    "networks": {"operating systems"},
    "operating systems": {"data structures", "computer organization"},
    "programming languages": {"data structures", "computer organization"},
}

func main() {
    for i, course := range topoSort(prereqs) {
        fmt.Printf("%d:	%s
", i+1, course)
    }
}

func topoSort(m map[string][]string) []string {
    var order []string
    seen := make(map[string]bool)
    var visitAll func(items []string)
    visitAll = func(items []string) {
        for _, item := range items {
            if !seen[item] {
                seen[item] = true
                visitAll(m[item])
                order = append(order, item)
            }
        }
    }
    var keys []string
    for key := range m {
        keys = append(keys, key)
    }
    sort.Strings(keys)
    visitAll(keys)
    return order
}

当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。

 

13:下面是一个比较容易出错的例子: 

        list := []int{1,2,3,4,5}
    var printfuncs []func()
    
    for _, d := range list {
        d2 := d // NOTE: necessary!

        printfuncs = append(printfuncs, func() {
            fmt.Println(d2)
        })
    }
    // ...do some work…
    for _, printd := range printfuncs {
        printd() 
    }

为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是直接使用循环变量d?

在上面的程序中,for循环语句引入了新的词法块,循环变量d在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以d为例,后续的迭代会不断更新d的值,当删除操作执行时,for循环已完成,d中存储的值等于最后一次迭代的值。这意味着,每次对printd的调用都是用的同一个变量。

为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。

 

如果你使用go语句或者defer语句会经常遇到此类问题。这不是go或defer本身导致的,而是因为它们都会等待循环结束后,再执行函数值。

 

14:可变参数

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

func fun(s string, vals ...int){
    fmt.Println("s is ", s)
    fmt.Println("vals is ", vals)
}

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

    fun("1", 1)
    fun("1,2,3", 1,2,3)

结果是:

s is  1
vals is  [1]
s is  1,2,3
vals is  [1 2 3]

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

list := []int{2,3,4}
fun("2,3,4", list...)

结果是:

s is  2,3,4
vals is  [2 3 4]

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

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

…interfac{}表示函数的最后一个参数可以接收任意类型,会在第7章详细介绍。

func fun(s string, vals ...interface{}) {
    fmt.Println("s is ", s)
    fmt.Println("vals is ", vals)
    fmt.Printf("%T
", vals)
    fmt.Println("len is ", len(vals))
}

func main() {
    fun("1", 1,2,'3',3.4,3i,"123")
}

结果是

s is  1
vals is  [1 2 51 3.4 (0+3i) 123]
[]interface {}
len is  6

15:defer

函数结束时,一般需要释放函数中申请的资源,随着函数变得复杂,维护清理逻辑变得越来越困难。而Go语言独有的defer机制可以让事情变得简单。你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

 

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

func title(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    ct := resp.Header.Get("Content-Type")
    if ct != "text/html" && !strings.HasPrefix(ct,"text/html;") {
        return fmt.Errorf("%s has type %s, not text/html",url, ct)
    }
    doc, err := html.Parse(resp.Body)
    if err != nil {
        return fmt.Errorf("parsing %s as HTML: %v", url,err)
    }
    // ...print doc's title element…
    return nil
}

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机制也常被用于记录何时进入和退出函数。下例中的bigSlowOperation函数,通过调用trace记录函数的被调情况。bigSlowOperation被调时,trace会返回一个函数值,该函数值会在bigSlowOperation退出时被调用。通过这种方式,可以记录函数的运行时间:

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

defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值。

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d
", x,result) }()
    return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

func triple(x int) (result int) {
    defer func() { result += x }()
    return double(x)
}
fmt.Println(triple(4))  // "12"

在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。下面的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。

for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close() // NOTE: risky; could run out of file descriptors
    // ...process f…
}

16:当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。

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

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

f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.f(0x0)
        /root/devel/go/src/testcode/test.go:16 +0x1b1
main.f(0x1)
        /root/devel/go/src/testcode/test.go:18 +0x180
main.f(0x2)
        /root/devel/go/src/testcode/test.go:18 +0x180
main.f(0x3)
        /root/devel/go/src/testcode/test.go:18 +0x180
main.main()
        /root/devel/go/src/testcode/test.go:22 +0x2a

不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。当某些不应该发生的场景发生时,我们就应该调用panic。比如,当程序到达了某条逻辑上不可能到达的路径:

        switch s := suit(drawCard()); s {
        case "Spades":                                // ...
        case "Hearts":                                // ...
        case "Diamonds":                              // ...
        case "Clubs":                                 // ...
        default:
            panic(fmt.Sprintf("invalid suit %q", s)) // Joker?
    }

为了方便诊断问题,runtime包允许程序员输出堆栈信息。在下面的例子中,我们通过在main函数中延迟调用printStack输出堆栈信息。

func main() {
    defer printStack()
    f(3)
}
func printStack() {
    var buf [4096]byte
    n := runtime.Stack(buf[:], false)
    fmt.Println("printStack output:")
    os.Stdout.Write(buf[:n])
}

结果如下:

f(3)
f(2)
f(1)
defer 1
defer 2
defer 3
printStack output:
goroutine 1 [running]:
main.printStack()
        /root/devel/go/src/testcode/test.go:30 +0x5b
panic(0x4a2700, 0x52a060)
        /usr/local/go/src/runtime/panic.go:491 +0x283
main.f(0x0)
        /root/devel/go/src/testcode/test.go:18 +0x1b1
main.f(0x1)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.f(0x2)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.f(0x3)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.main()
        /root/devel/go/src/testcode/test.go:25 +0x46
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.f(0x0)
        /root/devel/go/src/testcode/test.go:18 +0x1b1
main.f(0x1)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.f(0x2)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.f(0x3)
        /root/devel/go/src/testcode/test.go:20 +0x180
main.main()
        /root/devel/go/src/testcode/test.go:25 +0x46

15:通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。比如,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。

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

 

以语言解析器为例,说明recover的使用场景。考虑到语言解析器的复杂性,当某个异常出现时,我们不会选择让解析器崩溃,而是会将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之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。

基于以上原因,安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。为了标识某个panic是否应该被恢复,可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理。

下面的例子是title函数的变形,如果HTML页面包含多个 <title> ,该函数会给调用者返回一个错误(error)。在soleTitle内部处理时,如果检测到有多个 <title> ,会调用panic,阻止函数继续递归,并将特殊类型bailout作为panic的参数。

func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}
    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
        }
    }()
    // Bail out of recursion if we find more than one nonempty title.
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" &&
            n.FirstChild != nil {
            if title != "" {
                panic(bailout{}) // multiple titleelements
            }
            title = n.FirstChild.Data
        }
    }, nil)
    if title == "" {
        return "", fmt.Errorf("no title element")
    }
    return title, nil
}

在上例中,deferred函数调用recover,并检查panic value。当panic value是bailout{}类型时,deferred函数生成一个error返回给调用者。当panic value是其他non-nil值时,表示发生了未知的panic异常,deferred函数将调用panic函数并将当前的panic value作为参数传入;此时,等同于recover没有做任何操作。

原文地址:https://www.cnblogs.com/gqtcgq/p/8005439.html