函数
- 在 Go 中,函数是一等公民。
- 也就是说,函数可以作为一个值,在其他的函数之间**传递、赋予变量、类型判断和转换等。
- 更深一层来说,函数可以作为一个随意传播的逻辑组件。
package main
import "fmt"
type Printer func(contents string) (n int, err error)
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer
p = printToStd
p("something")
}
- 在上面的例子中,声明了一个函数类型,叫做 Printer(请注意它的写法)。
- 函数签名:一个函数的参数列表和结果列表的统称。
- 如果两个函数的参数列表和结果列表中的元素顺序和类型是一直的,那它们就是同一个类型的函数(函数名、参数名无关)。
- 在上面的代码中,main 代码块展示了它们之间可以存在的调用关系。
编写高阶函数
- 高阶函数:将其他函数作为参数或者返回值的函数。
package main
import (
"errors"
"fmt"
)
type operate func(x, y int) int
// 方案1。
func calculate(x int, y int, op operate) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
// 方案2。
type calculateFunc func(x int, y int) (int, error)
func genCalculator(op operate) calculateFunc {
return func(x int, y int) (int, error) {
if op == nil {
return 0, errors.New("invalid operation")
}
return op(x, y), nil
}
}
func main() {
// 方案1。
x, y := 12, 23
op := func(x, y int) int {
return x + y
}
result, err := calculate(x, y, op)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
result, err = calculate(x, y, nil)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
// 方案2。
x, y = 56, 78
add := genCalculator(op)
result, err = add(x, y)
fmt.Printf("The result: %d (error: %v)\n",
result, err)
}
- 上面的代码为你展示了将函数作为参数和返回值的过程
使用高级函数的意义
- 在一个高级函数中,它的内部逻辑并不是完整的,有一部分的逻辑需要作为参数(自由变量)的函数完成。
- 这个自由变量代表了什么,并不是在定义这个高级函数的时候确定的,而是它被调用的时候确定的。
- 这样的意义在于,我们延迟了一部分程序逻辑功能(表面上),动态地生成了那部分逻辑(实际上)。
函数总结
- 理解高级函数的意义,理解逻辑动态生成的意义。
- 要知道 Go 如何鉴别一个函数,以及函数签名的作用。
- 注意函数传参,如果是引用类型,要关注它何时被修改。因为这可能会影响到程序的稳定。
- 一个原则:不要把你的程序细节暴露给外界,也尽量不要让外界的变动影响到你的程序。(根据这个原则,你会理解函数闭包的作用。
结构体
- 结构体在我看来包含两个部分:一个是结构体中包含的数据,另一个是它持有的方法。
- 把接口提类型中的一个字段看做是一个属性或者一个数据,再把隶属于它的一个方法看做是附加在其中数据之上的一个能力或者操作。
- 上面的封装,是面向对象编程的一个主要原则。
// AnimalCategory 代表动物分类学中的基本分类法。
type AnimalCategory struct {
kingdom string // 界。
phylum string // 门。
class string // 纲。
order string // 目。
family string // 科。
genus string // 属。
species string // 种。
}
func (ac AnimalCategory) String() string {
return fmt.Sprintf("%s%s%s%s%s%s%s",
ac.kingdom, ac.phylum, ac.class, ac.order,
ac.family, ac.genus, ac.species)
}
category := AnimalCategory{species: "cat"}
fmt.Printf("The animal category: %s\n", category) // 打印:The animal category: cat
- 上面的例子,展现了这种封装特性。
- 特别的,String 方法是一个特定的方法,它会在调用 fmt.Ptinf 函数的时候被调用。(有点像 Python 中的魔法方法)
组合(而非继承)
- Go 中没有继承,但是你可以使用组合达到类似的效果。
type Animal struct {
scientificName string // 学名。
AnimalCategory // 动物基本分类。
}
- 在上面的 Animal 类型中,包含了 AnimalCategory 的所有信息和方法,你可以用 . 调用他们:
func (a Animal) Category() string {
return a.AnimalCategory.String()
}
- 但是,如果你在自己的结构体中实现了一个与组合重复的方法,则它将“覆盖”组合中的方法。当然,你可以将它们一层一层包装起来:
func (a Animal) String() string {
return fmt.Sprintf("%s (category: %s)",
a.scientificName, a.AnimalCategory)
}
-
你可以用下面的图来加深理解:
image.png
- 如果存在多层嵌套组合,则方法的调用会依据嵌套深度确定。
- 如果同一级别的组合出现了相同的字段或方法,会出现编译错误。
值方法和指针方法
- 上面提到的都是值方法,下面是一个指针方法的例子:
func (cat *Cat) SetName(name string) {
cat.name = name
}
- 他们的区别在于是否有符号 *。
- 一般情况下,我们都使用指针方法。但是,你依然需要了解他们的区别(这里就直接复制了):
- 值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。
而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,却一定会体现在原值上。 - 一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法。
严格来讲,我们在这样的基本类型的值上只能调用到它的值方法。但是,Go 语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。
比如,在Cat类型的变量cat之上,之所以我们可以通过cat.SetName("monster")修改猫的名字,是因为 Go 语言把它自动转译为了(&cat).SetName("monster"),即:先取cat的指针值,然后在该指针值上调用SetName方法。 - 在后边你会了解到,一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。如果一个基本类型和它的指针类型的方法集合是不同的,那么它们具体实现的接口类型的数量就也会有差异,除非这两个数量都是零。
比如,一个指针类型实现了某某接口类型,但它的基本类型却不一定能够作为该接口的实现类型。
- 对于两者的区别,你可以通过下面的例子感受一下:
package main
import "fmt"
type Cat struct {
name string // 名字。
scientificName string // 学名。
category string // 动物学基本分类。
}
func New(name, scientificName, category string) Cat {
return Cat{
name: name,
scientificName: scientificName,
category: category,
}
}
func (cat *Cat) SetName(name string) {
cat.name = name
}
func (cat Cat) SetNameOfCopy(name string) {
cat.name = name
}
func (cat Cat) Name() string {
return cat.name
}
func (cat Cat) ScientificName() string {
return cat.scientificName
}
func (cat Cat) Category() string {
return cat.category
}
func (cat Cat) String() string {
return fmt.Sprintf("%s (category: %s, name: %q)",
cat.scientificName, cat.category, cat.name)
}
func main() {
cat := New("little pig", "American Shorthair", "cat")
cat.SetName("monster") // (&cat).SetName("monster")
fmt.Printf("The cat: %s\n", cat)
cat.SetNameOfCopy("little pig")
fmt.Printf("The cat: %s\n", cat)
type Pet interface {
SetName(name string)
Name() string
Category() string
ScientificName() string
}
_, ok := interface{}(cat).(Pet)
fmt.Printf("Cat implements interface Pet: %v\n", ok)
_, ok = interface{}(&cat).(Pet)
fmt.Printf("*Cat implements interface Pet: %v\n", ok)
}
------------------
The cat: American Shorthair (category: cat, name: "monster")
The cat: American Shorthair (category: cat, name: "monster")
Cat implements interface Pet: false
*Cat implements interface Pet: true
QianhengdeMacBook-Pro:q3 qianhengtan$
接口
- 接口无法被实例化,也就是说它无法通过 new 和 make 创建出来。
- 对于任何数据类型,只要它的方法集合中包含了一个接口的全部特征,它就一定是这个接口的实现类型。例如:
package main
import "fmt"
type Pet interface {
SetName(name string)
Name() string
Category() string
}
type Dog struct {
name string // 名字。
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func main() {
// 示例1。
dog := Dog{"little pig"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok) // false
_, ok = interface{}(&dog).(Pet)
fmt.Printf("*Dog implements interface Pet: %v\n", ok) // true
fmt.Println()
// 示例2。
var pet Pet = &dog
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name()) // little pig
}
- 在上面的例子中,*Dog 就是 Pet 的一个实现类型。
注意,Dog 不是 Pet 的实现类型,因为 Dog 中没有SetName 这个方法(Dog只包含值方法),而 *Dog 包含。
- 确定数据类型的方法和接口中的方法是否相等,需要满足两个条件:
- 两个方法的签名完全一致。(参数和返回值的类型和顺序)
- 方法名称。
- 对于一个接口类型的变量来说(上面的 pet),我们赋给它的值为动态值,相应的类型为动态类型。显然,一个接口变量的动态类型可以是很多种。
为接口变量赋值会发生什么
- 会发生两件事:第一件事:赋值过程是值的复制,这一点你应该很明白。
- 第二件事:接口变量的值并不等同于这个可被称为动态值的副本。它会包含两个指针,一个指针指向动态值,一个指针指向类型信息。这个数据结构叫做 iface。
- 所以,接口变量的结构会发生改变,这需要引起你的警觉,比如他们的数据值为 nil 的时候,仔细研究下面的例子,你会有所感悟。
var dog1 *Dog
fmt.Println("The first dog is nil.")
dog2 := dog1
fmt.Println("The second dog is nil.")
var pet Pet = dog2
if pet == nil {
fmt.Println("The pet is nil.")
} else {
fmt.Println("The pet is not nil.") // 这一句会被执行
}
fmt.Printf("The type of pet is %T.\n", pet)
fmt.Printf("The type of pet is %s.\n", reflect.TypeOf(pet).String())
fmt.Printf("The type of second dog is %T.\n", dog2)
fmt.Println()
网友评论