【Go】函数、方法和接口
函数、方法和接口
- 具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数闭包函数是函数式编程语言的核心。
- 方法是绑定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定。
- 接口定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定的。Go语言通过隐式接口机制实现了面向对象模型。
Go语言程序的初始化和执行总是从 main.main 函数开始的。但是如果 main 包导入了其它的包,则会按照顺序将它们包含进main包里。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来。
然后创建和初始化这个包的常量和变量,再调用包里的 init 函数。同一个文件内的多个 init 则是以出现的顺序依次调用( init 不是普通函数,可以定义有多个,所以也不能被其它函数调用)。
最后,当 main 包的所有包级常量、变量被创建和初始化完成,并且 init 函数被执行后,才会进入 main.main 函数,程序开始正常执行。下图是Go程序函数启动顺序的示意图:
函数
// 具名函数
func Add(a, b int) int {
return a+b
}
// 匿名函数
var Add = func(a, b int) int {
return a+b
}
Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
func Sum(a int, more ...int) int {
for _, v := range more {
a += v
}
return a
}
Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了 Len 或 Cap 信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的append 必须要返回一个切片的原因。
Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。
Go1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。不过连续动态栈也带来了新的
问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。
方法
// 关闭文件
func (f *File) CloseFile() error {
// ...
}
// 读文件数据
func (f *File) ReadFile(offset int64, data []byte) int {
// ...
}
var CloseFile = (*File).CloseFile
// 不依赖具体的文件对象
// func ReadFile(f *File, offset int64, data []byte) int
var ReadFile = (*File).ReadFile
f, _ := OpenFile("foo.dat")
ReadFile(f, 0, data)
CloseFile(f)
这样的话, CloseFile 和 ReadFile 函数就成了 File 类型独有的方法了(而不是 File 对象方法)。它们也不再占用包级空间中的名字资源。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。
Go语言不支持传统面向对象中的继承特性,而是以自己特有的组合方式支持了方法的继承。Go语言中,通过在结构体内置匿名的成员来实现继承:
import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
虽然我们可以将 ColoredPoint 定义为一个有三个字段的扁平结构的结构体,但是我们这里将 Point 嵌入到 ColoredPoint 来提供 X 和 Y 这两个字段。所有继承来的方法的接收者参数依然是那个匿名成员本身,而不是当前的变量。
接口
Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
type io.Writer interface {
Write(p []byte) (n int, err error)
}
type error interface {
Error() string
}
type UpperWriter struct {
io.Writer
}
func (p *UpperWriter) Write(data []byte) (n int, err error) {
return p.Writer.Write(bytes.ToUpper(data))
}
func main() {
fmt.Fprintln(&UpperWriter{os.Stdout}, "hello, world")
}