go 函数与方法

函数

函数是一段能够完成特定功能的代码段,可以接收输入参数或者能够返回想要的值。

声明

func funcName(paramName type) returntype {
    // 函数体
}

特性

  • 函数允许有多个返回值。

  • 在函数定义的时候,可以给所有的返回值分别命名,Go 会自动创建这些变量,在函数结束时,直接使用return就可以将返回值返回。

  • 函数参数只有值传递,没有引用传递,即全部需要重新拷贝变量,可通过传递指针或者引用类型变量实现引用传递的效果。

  • 函数也是一种类型。如果两个函数的入参类型和返回值类型均相同,则它们是同种类型。在将函数作为参数传递给另一个函数时或将函数作为返回值时,函数类型就会显得很有用了。

    type calculateFunc func(int,int) int
    func calculate(a,b int,f calculateFunc) (r int) {
        r = f(a,b)
        return
    }
    
  • 函数不能重载(函数名相同,入参等不同)。

可变参数函数

可变参数函数是接收可变数量的参数的函数。只允许最后一个参数是可变参数。

Go 中的一些内置函数都是可变参数函数,如 append() 函数:

func append(slice []Type, elems ...Type) []Type

在上面的函数声明中,elems 就是可变参数,可以接收任意数目的实参。

可变参数的工作原理就是把可变参数转为切片,然后传入函数中。在函数中打印可变参数的类型和值,可见传入函数中的 elems 实际是 []Type 类型的切片。

可以不传可变参数,这种情况下,elems 是一个长度和容量都为 0 的 nil 切片。

切片传递给可变参数函数

func change(s ...string) {
    fmt.Printf("%T\n",s)
    fmt.Println(s)
}

func main() {
    slice := []string{"Hello","World","Go"}
    change(slice...)    // 使用语法糖,切片加上后缀 ...
}

使用语法糖,直接在切片后加上 ... 后缀,可将切片直接传入函数,不会根据可变参数创建新的切片。也因为这样,在函数中改变切片的值,会影响到函数外的切片。

也可以通过将数组转化成切片传递给可变函数参数。

func change(s ...string) {
    fmt.Printf("%T\n",s)
    fmt.Println(s)
}

func main() {
    slice := [3]string{"Hello","World","Go"}
    change(slice[:]...)
}

匿名函数

函数可以作为一个值,这样便可以将函数赋给一个变量或者返回一个函数变量。

var add = func(a,b int) (r int) {
    r = a + b
    return
}

func main() {
    fmt.Println("a + b =",add(1,2))
}

上面的代码,创建了一个匿名函数,赋给了一个全局变量add。Go 编译器会自动判别变量add的类型,这里是func(int, int) int

匿名函数可以赋值,也可以立即执行:

func main() {
    sum := func(a,b int) int {
        return a + b
    }(1,2)
    fmt.Println(sum)
}

内建函数

  • new 和 make

new(T)make(T, args) 是 Go 语言内建函数,用来分配内存,但适用的类型不一样。new 为 T 类型的变量分配已置零的内存空间,并返回地址(变量是一个指针),适用于值类型,如 int、数组、结构体等。make 得到的是初始化之后的 T 的引用,用来分配引用类型,只适用于 slice、map 和 channel。

当使用 new 创建对象时返回的是对象的地址,所以如果你将 new 返回值作为参数传递,实际传递的是对象的引用。在被调函数中,对象的修改会影响到对象的原始值。

通过 make 创建的切片可以 copy,其他方式不行。

通过 make 创建的 map 是空 map,通过字面量形式创建的 map 是 nil,切片同理。

使用 make 创建 map 变量时可以指定第二个参数,不过会被忽略。

  • cap 和 len

cap 获取容量,len 获取长度。

cap 适用于数组、数组指针、slice 和 channel。不适用于 map。

可以用 len 返回 map 的元素个数。

  • close

主要用来关闭 channel。

  • append

用来追加元素到数组、slice 中。

  • panic 和 recover

用来做错误处理。

todo:panic,recover,defer 的关系。

当协程遇到 panic 时,会遍历该协程的 defer 并依次执行(执行顺序是先进后出)。如果在 defer 执行的过程遇到 recover 则停止 panic,返回 recover 继续向下执行,如果没有遇到 recover,遍历完 defer,然后向 stderr 抛出 panic 信息。如果执行第一个 defer 出现异常,顺序执行下一个 defer?

方法

声明

func (receiver Type) funcName(...Type) Type {...} 

方法声明和函数类似,区别在于:方法声明时,在 func 和方法名之间会增加一个额外的参数(receiver Type),其中 receiver 称为接收者,Type 可以是任意合法的类型。可以说,该方法属于类型 Type。

即:方法属于某一种类型,且有接收者。

特性

  • 方法的接收者是调用者的副本。

  • 我们可以在方法内部访问 receiver 的每一个成员。

  • 必须保证类型和其方法定义在同一个包里。如果做不到,可以创建类型别名。

  • 每个方法声明的时候,编译器会各自声明相对应的隐式函数。

  • 不能用多级指针调用方法。

值接收者方法和指针接收者方法

接收者类型为 T 的方法称为值接收者方法;接收者类型为 *T 的方法称为指针接收者方法。

其中 T 必须满足一下条件:

  • T 必须是自定义类型。
  • T 的定义必须和方法的声明在同一个包内。
  • T 不能是接口类型或者接口指针类型。

值接收者方法和指针接收者方法最大区别在于,在方法中修改指针接收者的值会影响到调用者的值(除非这个类型是应用类型的别名类型,如切片),而修改值接收者的值不会。因为方法的接收者是调用者的副本,一个是值的副本,一个是指针的副本,指针的副本指向的还是原来的值。

值方法和指针方法的使用场景

根据场景考虑使用值接收者还是指针接收者方法。

  • 使用值方法时,如果变量很大,拷贝成本高,这时应考虑使用指针方法。
  • 确认需要在方法中直接修改调用者的值时,使用指针方法;否则,可能会造成混乱,随着代码的增长,很容易就会出现错误,因为调用堆栈深处的某个地方改变了指针指向的值。

当确认需要在方法中直接修改调用者的值,或值接收者变量很大,拷贝成本高时,应考虑使用指针接收者方法。其他情况建议使用值接收者方法。

方法集

方法集是一组关联到自定义类型的值或指针的方法。

一个自定义类型 T 的方法集仅包括它的值接收者方法,而该类型的指针类型 *T 的方法集包括它的值接收者方法和指针接收者方法。

调用指针接收者方法也可以写成值调用a.A()形式,编译器会自动帮我们转成指针调用(&a).A(),以满足接收者的要求。调用值接收者方法也可以写成指针调用,因为值接收者方法属于该类型的指针类型 *T 的方法集。

值接收者的方法可以使用值或者指针调用,而对于指针接收者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。编译器不总能自动获得到一个值的地址,要保证可寻址的结构体才可以调用该结构体指针接收者的方法。如下代码会报错:

// 定义接口 notifier
type notifier interface {
    notify()
}

type user struct {
    name string
    email string
}

func (u *user)notify()  {
    fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}

// 接收一个 notifier 接口类型的参数,并发送通知
func sendNotification(n notifier)  {
    n.notify()
}

func main() {
    u := user{"Seeklaod", "[email protected]"}
    sendNotification(u)     // 编译器不能自动获得到值的地址,不能自动转译
}

报错信息为:

cannot use u (type user) as type notifier in argument to sendNotification:
user does not implement notifier (notify method has pointer receiver)

非结构体类型的方法

可以在 Go 任一合法类型上定义方法。

package main
import "fmt"

type myInt int

func (i myInt) echo ()  {
    fmt.Println(i)
}

func main() {
    var a myInt
    a = 20
    a.echo()
}

但必须保证类型和方法定义在同一个包里,否则编译报错。对于一些基本类型如 int,可以创建类型别名。

为什么有了函数还需要方法

  1. Go 不是纯粹的面向对象的语言且不支持类,通过类型的方法可以实现和类相似的功能,又不会像类那样显得很“重”;
  2. 同名的方法可以定义在不同的类型上,但是函数名不允许相同。

你可能感兴趣的:(go 函数与方法)