函数

1. 定义
2. 参数
3. 返回值
4. 匿名函数
5. 延迟调用
6. 错误处理 
  • 定义

函数是结构化编程中最小的模块单元,日常开发过程中,将复杂的算法过程分解为若干个小任务,使程序的结构性更清晰,程序可读性提升,易于后期维护和让别人读懂你的代码。 另外可以把重复性的任务抽象成一个函数,可以更好的重用你的代码。

Go语言中使用关键词func来定义一个函数,并且左花括号不能另起一行,比如:

func hello(){   //左花括号不能另起一行
    println("hello")
}

Go语言中定义和应用函数时,有如下几点需要注意的点:

 函数无须前置声明
 不支持命名嵌套定义,支持匿名嵌套
 函数只能判断是否为nil,不支持其它比较操作
 支持多返回值
 支持命名返回值
 支持返回局部变量指针
 支持匿名函数和闭包
func hello()
{               //左括号不能另起一行
}

func add(x,y int) (sum int){    //命名返回值
    sum = x + y
    return
}

func vals()(int,int){       //支持多返回值
    return 2,3
}

func a(){}
func b(){}

func add(x,y int) (*int){      //支持返回局部变量指针
    sum := x + y
    return &sum
}

func main(){
    println(a==b)       //只能判断是否为nil,不支持其它比较操作
    func hello() {      //不支持命名嵌套定义
        println("hello")
    }
}

具备相同签名(参数和返回值)的函数才视为同一类型函数,比如:

func hello() {
    fmt.Println("hello")
}
func say(f func()){                                                                                                                                   
    f()
}

func main(){
    f := hello
    say(f)
}
  • 参数 Go语言中给函数传参时需要注意以下几点:
 不支持默认参数
 不支持命名实参
 参数视作为函数的局部变量
 必须按签名顺序传递指定类型和数量的实参
 相邻的同类型参数可以合并
 支持不定长变参,实质上是slice
func test(x,y int, s string, _ bool){   //相邻的同类型参数可以合并
    return
}

func add(x ,y int)int{  //参数视作为函数的局部变量
    x := 100            //no new variables on left side of :=
    var y int = 200     //y redeclared in this block
    return x +y
}

func main(){
    //test(1,2,"s")       //not enough arguments in call to test                                                                                                                             
    test(1,2,"s",false)
}

不管传递的是指针、引用还是其它类型参数,都是值拷贝传递的,区别在于拷贝的目标是目标对象还是拷贝指针而已。 在函数调用之前,编译器会为形参和返回值分配内存空间,并将实参拷贝到形参内存。比如:

func test1(x *int){
    fmt.Printf("%p, %v\n",&x ,x)
}

func main(){
    a := 0x100
    p := &a
    fmt.Printf("%p, %v\n", &p, p)
    test1(p)
}
输出:
0xc42002c020, 0xc42000a320
0xc42002c030, 0xc42000a320
从结构中看出, 实参和形参指向同一目标,但是传递的指针是被赋值了的

如果函数参数过多,可以将参数封装成一个结构体类型,比如:

type serverOption struct{
    addr    string
    port int 
    path    string
    timeout time.Duration
}

func newOption() * serverOption{
    return &serverOption{
        addr:"127.0.0.1",
        port:8080,
        path:"/var/www",
        timeout: time.Second * 5,
    }   
}
func (s *serverOption)server(){
    println("run server")
}

func main(){
    s := newOption()
    s.port = 80
    s.server()
    for{}                                                                                                                                             
}
  • 变参 变参本质上是一个切片(slice),只能接收一到多个同类型参数,且必须放在参数列表尾部,比如:
func add(args ...int) int {
  total := 0
  for _, v := range args {
    total += v
  }
  return total
}
func main() {
  fmt.Println(add(1,2,3))
}

实质上是切片,那是否可以直接传个切片或数组呢?

func test1(s string, a ...int){
    fmt.Printf("%T, %v\n", a, a)    //[]int, [1 2 3 4]
}
{
    a := [4]int{1,2,3,4}
    test1("s", a)     //cannot use a (type [4]int) as type int in argument to test1                                                                                                                               
    test1("s", a[:]     //cannot use a[:] (type []int) as type int in argument to test1
    test1("s", a[:]...) //切片展开
}

变参既然是切片,那么参数复制的是切片的本身,并不包括底层的数组,因此可以修改原数据, 也可以copy底层数据,防止原数据被修改,比如:

func test1(a ...int){
    for i := range a{
        a[i] += 100
    }
}
func main(){
    a := [4]int{1,2,3,4}
    // b := make([]int,0)
    // copy(b,a[:])         
    // test1(b[:]...)
    test1(a[:]...)                                                                                                                                                 
    for i := range a{
        fmt.Println(a[i])
    }
}
  • 匿名函数 匿名函数就是没有定义函数名称的函数。我们可以在函数内部定义匿名函数,也叫函数嵌套。 匿名函数可以直接被调用,也可以赋值给变量、作为参数或返回值。比如:
func main(){
    func(s string){     //直接被调用
        println(s)
    }("hello gopher!!!")
    /*
    func(s string){     //未被调用
        println(s)
    }
    */
}

func main(){
    hi := func(s string){   //赋值给变量
        println(s)
    }                                                                                                                                                 
    hi("hello gopher!!!")
}

func test(f func(string)){
    f("hello gopher!!!")
}
func main(){
    hi := func(s string){
        println(s)
    }
    test(hi)     //作为参数                                                                                                                                     
}

func test()func(string){
    hi := func(s string){   //作为返回值
        println(s)
    }
    return hi
}

func main(){
    f := test()                                                                                                                                       
    f("hello gopher!!!")
}

普通函数和匿名函数都可以作为结构体的字段,比如:

{
    type calc struct{
        mul func(x,y int)int
    }
    x := calc{
        mul: func(x,y int)int{                                                                                                                        
            return (x*y)
        },
    }
    println(x.mul(2,3))
}

也可以经通道传递,比如:

{
    c := make(chan func(int, int)int, 2)
    c <- func(x,y int) int {return x + y}
    println((<-c)(2,3))
}
  • 闭包(closure) 是指在上下文中引用了自由变量(未绑定到特定对象)的代码块(函数),或者说是代码块(函数)与和引用环境的组合体。
func intSeq()func()int{
    i := 0
    println(&i)
    return func()int{
        i += 1
        println(&i,i)
        return i
    }   
}
                                                                                                                                                      
func main(){
    nextInt := intSeq()
    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())

    newInt := intSeq()
    fmt.Println(newInt())
}
输出:
0xc42000a320
0xc42000a320 1
1
0xc42000a320 2
2
0xc42000a320 3
3
0xc42007a010
0xc42007a010 1
1

当nextInt函数返回后,通过输出指针,我们可以看出函数在main运行时,依然引用的是原环境变量指针,这种现象称作闭包。 所以说,闭包是函数和引用环境变量的组合体。正因为闭包是通过指针引用环境变量,那么就会导致该变量的生命周期 变长,甚至被分配到堆内存。如果多个匿名函数引用同一个环境变量,会让事情变得更加复杂,比如:

func test()[]func(){
    var s []func()
    for i:= 0;i < 3;i++{
        s =  append(s, func(){
            println(&i , i)
        })
    }
    return s
}
func main(){
    funcSlice := test()
    for _ , f := range funcSlice{
        f()
    }
}
输出:
0xc42000a320 3
0xc42000a320 3
0xc42000a320 3

解决方法就是每次用不同的环境变量或参数赋值,比如修改后的test函数:

func test()[]func(){
    var s []func()
    for i:= 0;i < 3;i++{
        x := i
        s =  append(s, func(){
            println(&x , x)                                                                                                                           
        })
    }
    return s
}

闭包在不用传递参数的情况下就可以读取和修改环境变量,当然我们是要为这种遍历付出代价的,所以日常开发中,在高并发服务 的场景下建议慎用,除非你明确你的需求必须这样做。

  • 延迟调用 Go语言提供defer关键字,用于延迟调用,延迟到当函数返回前被执行,多用于资源释放、解锁以及错误处理等操作。
func main() {
    f, err := createFile("defer.txt")
    if err != nil {
        fmt.Println(err.Error())
        return
    }   
    defer closeFile(f)
    writeFile(f)
}

func createFile(filePath string) (*os.File, error) {
    f, err := os.Create(filePath)
    if err != nil {
        return nil, err 
    }   
    return f, nil 
}

func writeFile(f *os.File) {
    fmt.Println("write file")
    fmt.Fprintln(f, "hello gopher!")
}

func closeFile(f *os.File) {
    fmt.Println("close file")
    f.Close()
}

如果一个函数内引用了多个defer,它们的执行顺序是怎么样的呢?

package main

func main() {
	defer println("a")
	defer println("b")
}
输出:
b
a

如果函数中引入了panic函数,那么延迟调用defer会不会被执行呢?

func main() {
    defer println("a")
    panic("d")                                                                                                                                        
    defer println("b")
}

日常开发中,一定要记住defer是在函数结束时才被调用的,如果应用不合理,可能会造成资源浪费,给gc带来压力,甚至 造成逻辑错误,比如:

func main() {
    for i := 0;i < 10000;i++{
        filePath := fmt.Sprintf("/data/log/%d.log", i)
        fp, err := os.Open(filePath)
        if err != nil{
            continue
        }
        defef fp.Close()    //这是要在main函数返回时才会执行的,不是在循环结束后执行,延迟调用,导致占用资源
        //do stuff...
    }
}

修改方案是直接调用Close函数或将逻辑封装成独立函数:
func logAnalisys(p string){
    fp, err := os.Open(p)
    if err != nil{
        continue
    }
    defef fp.Close()
    //do stuff
}

func main() {
    for i := 0;i < 10000;i++{
        filePath := fmt.Sprintf("/data/log/%d.log", i)
        logAnalisys(filePath)
    }
}

在性能方面,延迟调用花费的代价也很大,因为这个过程包括注册、调用等操作,还有额外的内存开销。比如:

package main

import "testing"
import "fmt"
import "sync"

var m sync.Mutex

func test(){
	m.Lock()
	m.Unlock()
}

func testCap(){
	m.Lock()
	defer m.Unlock()
}

func BenchmarkTest(t *testing.B){
	for i:= 0;i < t.N; i++{
		test()
	}
}

func BenchmarkTestCap(t *testing.B){
	for i:= 0;i < t.N; i++{
		testCap()
	}
}

func main(){
	resTest := testing.Benchmark(BenchmarkTest)
	fmt.Printf("BenchmarkTest \t %d, %d ns/op,%d allocs/op, %d B/op\n", resTest.N, resTest.NsPerOp(), resTest.AllocsPerOp(), resTest.AllocedBytesPerOp())
	resTest = testing.Benchmark(BenchmarkTestCap)
	fmt.Printf("BenchmarkTestCap \t %d, %d ns/op,%d allocs/op, %d B/op\n", resTest.N, resTest.NsPerOp(), resTest.AllocsPerOp(), resTest.AllocedBytesPerOp())
}
输出:
BenchmarkTest 	 50000000, 27 ns/op,0 allocs/op, 0 B/op
estCap 	 20000000, 112 ns/op,0 allocs/op, 0 B/op

在要求高性能的高并发场景下,应避免使用延迟调用。 - 错误处理 Go语言标准库将将error定义为接口类型,以便实现自定义错误类型。比如:

type error interface{
    Error() string
}

按照Go语言编程习惯,error总是最后一个函数返回值,并且标准库提供了创建函数,可以方便的 创建错误消息的error对象。比如:

func divTest(x ,y int)(int, error){
    if y == 0{
        return 0, errors.New("division by zero")
    }   
    return x/y,nil
}
func main(){
    v, err := divTest(3,0)
    if err != nil{
        log.Fatalln(err.Error())
    }   
    println(v)                                                                                                                                        
}

日常开发中,我们需要根据需求自定义错误类型,可以存放更多的上下文信息,或者跟觉错误类型做出相应的错误处理。比如:

type NegativeError struct{
    x, y int
}
func (NegativeError)Error()string{
    return "negative value error"
}

type MolError struct {
    x, y int
}
func (MolError)Error()string{
    return "devision by zero"
}

func molTest(x ,y int)(int, error){
    if y == 0{
        return 0, MolError{x,y}
    }
    if x < 0 || y < 0{
        return 0, NegativeError{x,y}
    }
    return x%y,nil
}
func main(){
    v, err := molTest(3,-1)
    if err != nil{
        switch e := err.(type){     //获取错误类型
            case MolError:
                println(e.x,e.y)
            case NegativeError:
                println(e.x,e.y)
            default:
                println(e)
        }
        log.Fatalln(err.Error())
    }
    println(v)
}

与error相比,panic/recover在应用上更类似于try/catch结构化。

func panic() interface{}   
func recover() interface{}

panic 立即中断但前函数处理流程,执行延迟调用。recover在延迟调用中可以捕获并返回panic产生的错误对象,比如:

func Myrecover(){
    if err := recover(); err != nil{
        log.Fatalln(err)
    }   
}

func main(){
    println("start...")
    defer Myrecover()
    panic("dead")                                                                                                                                     
    println("end...")
}

如果有连续多次调用panic的场景,只有最后一次panic会被recover捕获处理,比如:

func Myrecover(){
    if err := recover(); err != nil{
        log.Fatalln(err)
    }   
}

func main(){
    defer Myrecover()
    defer func(){
        panic("a bad problem")
    }()
    panic("a problem")
}

recover只有在延迟调用函数中才能得到正常工作,比如:

func main() {
    defer Myrecover()
    defer log.Println(recover())
    defer println(recover())
    panic("a problem") 
}
输出:
(0x0,0x0)
2016/11/12 07:07:54 <nil>
2016/11/12 07:07:54 a problem
exit status 1

在日常开发过程中,经常需要进行调试,可以使用函数输出完整的调用栈信息,比如:

func Myrecover(){
    if err := recover(); err != nil{
        fmt.Println(err)
        debug.PrintStack()
        //log.Fatalln(err)
    }
}
func main(){
    defer Myrecover()
    panic("a problem")
}
输出:
a problem
goroutine 1 [running]:
runtime/debug.Stack(0xc42002c010, 0xc42003fe20, 0x1)
	/root/data/go/src/runtime/debug/stack.go:24 +0x79
runtime/debug.PrintStack()
	/root/data/go/src/runtime/debug/stack.go:16 +0x22
main.Myrecover()
	/root/data/gopath/test/panic.go:10 +0x85
panic(0x48a5e0, 0xc42000a320)
	/root/data/go/src/runtime/panic.go:458 +0x243
main.main()
	/root/data/gopath/test/panic.go:16 +0x8d

日常开发中,只有在系统发生了不可恢复性或无法正常工作的错误可以使用panic,比如端口号被占用、数据库未启动、文件系统错误等,否则不建议使用。