golang学习笔记---函数、方法和接口

时间:2021-08-18 06:45:29

函数:对应操作序列,是程序的基本组成元素。

函数有具名和匿名之分:
具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部
作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。方法是绑

定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静
态绑定

接口:定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对
应的方法是在运行时动态绑定的。

Go程序函数启动顺序的示意图:

golang学习笔记---函数、方法和接口

要注意的是,在 main.main 函数执行之前所有代码都运行在同一个goroutine,也
就是程序的主系统线程中。

因此,如果某个 init 函数内部用go关键字启动了新的
goroutine的话,新的goroutine只有在进入 main.main 函数之后才可能被执行到。

函数

在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名
和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例

// 具名函数
func Add(a, b int) int {
  return a+b
}
// 匿名函数
var Add = func(a, b int) int {
  return a+b
}

Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和
被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须
是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

// 多个参数和多个返回值
func Swap(a, b int) (int, int) {
  return b, a
}
// 可变数量的参数
// more 对应 []int 切片类型
func Sum(a int, more ...int) int {
  for _, v := range more {
    a += v
  }
  return a
}

 当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:

package main

import (
    "fmt"
)

func main() {
    var a = []interface{}{123, "abc"}
    Print(a...) // 123 abc
    Print(a)    // [123 abc]
}
func Print(a ...interface{}) {
    fmt.Println(a...)
}

第一个 Print 调用时传入的参数是 a... ,等价于直接调用 Print(123,
"abc") 。第二个 Print 调用传入的是未解包的 a ,等价于直接调
用 Print([]interface{}{123, "abc"}) 。

‘…’ 其实是go的一种语法糖。 
它的第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。 
第二个用法是slice可以被打散进行传递。

不仅函数的参数可以有名字,也可以给函数的返回值命名:

func Find(m map[int]int, key int) (value int, ok bool) {
value, ok = m[key]
return
}

如果返回值命名了,可以通过名字来修改返回值,也可以通过 defer 语句
在 return 语句之后修改返回值:

func Inc() (v int) {
defer func(){ v++ } ()
return 42
}

其中 defer 语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的
局部变量 v ,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式
访问,而是以引用的方式访问。

闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:

package main

import (
"fmt"
) func main() {
for i := 0; i < 3; i++ {
defer func() { fmt.Println(i) }()
}
}

// Output:
// 3
// 3
// 3

因为是闭包,在 for 迭代语句中,每个 defer 语句延迟执行的函数引用的都是
同一个 i 迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。
修复的思路是在每轮迭代中为每个 defer 函数生成独有的变量。可以用下面两种
方式:

package main

import (
"fmt"
) func main() {
for i := 0; i < 3; i++ {
i := i // 定义一个循环体内局部变量i
defer func() { fmt.Println(i) }()
}
}

  

package main

import (
"fmt"
) func main() {
for i := 0; i < 3; i++ {
// 通过函数传入i
// defer 语句会马上对调用参数求值
defer func(i int) { fmt.Println(i) }(i)
}
}

  

不过一般来说,在 for 循环内部执行 defer 语句并不是一个好
的习惯,此处仅为示例,不建议使用。

每个goroutine刚启动时只会分配
很小的栈(4或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以
达到GB级(依赖具体实现,在目前的实现中,32位体系结构为250MB,64位体系结构为1GB)

Go语言中指针不再是固定不变的了(因此
不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制
的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址

方法

Go语言的方法关联到类型的,这样可以在编译阶段完成方法的静态绑定。

我们可以给任何自
定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包
中,因此是无法给 int 这类内置类型添加方法的(因为方法的定义和类型的定义
不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函
数一样也不支持重载。

方法是由函数演变而来,只是将函数的第一个对象参数移动到了函数名前面了而
已。因此我们依然可以按照原始的过程式思维来使用方法。通过叫方法表达式的特
性可以将方法还原为普通类型的函数:

Go的接口类型是对其它类型行为的抽象和概括,

因为接口类型不会和特定的实现细
节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。
很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它
是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来
也像鸭子,那么就可以把它当作鸭子

fmt.Fprintf 函数的签
名如下:

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

其中 io.Writer 用于输出的接口, error 是内置的错误接口,

它们的定义如
下:

type io.Writer interface {
Write(p []byte) (n int, err error)
}
type error interface {
Error() string
}

  

我们可以通过定制自己的输出对象,将每个字符转为大写字符后输出 

package main

import (
"bytes"
"fmt"
"io"
"os"
) 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")
}