GO 入门指南
  • README
  • 开始
    • 前言
  • 第一部分:学习 Go 语言
    • 第1章:Go 语言的起源,发展与普及
      • 起源与发展
      • 语言的主要特性与发展的环境和影响因素
    • 第2章:安装与运行环境
      • 平台与架构
      • Go 环境变量
      • 在 Linux 上安装 Go
      • 在 Mac OS X 上安装 Go
      • 在 Windows 上安装 Go
      • 安装目录清单
      • Go 运行时(runtime)
      • Go 解释器
    • 第3章: 编辑器、集成开发环境与其它工具
      • Go 开发环境的基本要求
      • 编辑器和集成开发环境
      • 调试器
      • 构建并运行 Go 程序
      • 格式化代码
      • 生成代码文档
      • 其它工具
      • Go 性能说明
      • 与其它语言进行交互
  • 第二部分:语言的核心结构与技术
    • 第4章:基本结构和基本数据类型
      • 文件名、关键字与标识符
      • Go 程序的基本结构和要素
      • 常量
      • 变量
      • 基本类型和运算符
      • 字符串
      • strings 和 strconv 包
      • 时间和日期
      • 指针
    • 第5章:控制结构
      • if-else 结构
      • 测试多返回值函数的错误
      • switch 结构
      • for 结构
      • Break 与 continue
      • 标签与 goto
    • 第6章:函数(function)
      • 介绍
      • 函数参数与返回值
      • 传递变长参数
      • defer 和追踪
      • 内置函数
      • 递归函数
      • 将函数作为参数
      • 闭包
      • 应用闭包:将函数作为返回值
      • 使用闭包调试
      • 计算函数执行时间
      • 通过内存缓存来提升性能
    • 第7章:数组与切片
      • 声明和初始化
      • 切片
      • For-range 结构
      • 切片重组(reslice)
      • 切片的复制与追加
      • 字符串、数组和切片的应用
    • 第8章:Map
      • 声明、初始化和 make
      • 测试键值对是否存在及删除元素
      • for-range 的配套用法
      • map 类型的切片
      • map 的排序
      • 将 map 的键值对调
    • 第9章:包(package)
      • 标准库概述
      • regexp 包
      • 锁和 sync 包
      • 精密计算和 big 包
      • 自定义包和可见性
      • 为自定义包使用 godoc
      • 使用 go install 安装自定义包
      • 自定义包的目录结构、go install 和 go test
      • 通过 Git 打包和安装
      • Go 的外部包和项目
      • 在 Go 程序中使用外部库
    • 第10章:结构(struct)与方法(method)
      • 结构体定义
      • 使用工厂方法创建结构体实例
      • 使用自定义包中的结构体
      • 带标签的结构体
      • 匿名字段和内嵌结构体
      • 方法
      • 类型的 String() 方法和格式化描述符
      • 垃圾回收和 SetFinalizer
    • 第11章:接口(interface)与反射(reflection)
      • 接口是什么
      • 接口嵌套接口
      • 类型断言:如何检测和转换接口变量的类型
      • 类型判断:type-switch
      • 测试一个值是否实现了某个接口
      • 使用方法集与接口
      • 第一个例子:使用 Sorter 接口排序
      • 第二个例子:读和写
      • 空接口
      • 反射包
      • Printf 和反射
      • 接口与动态类型
      • 总结:Go 中的面向对象
      • 结构体、集合和高阶函数
  • 第三部分:Go 高级编程
    • 第12章:读写数据
      • 读取用户的输入
      • 文件读写
      • 文件拷贝
      • 从命令行读取参数
      • 用 buffer 读取文件
      • 用切片读写文件
      • 用 defer 关闭文件
      • 使用接口的实际例子:fmt.Fprintf
      • JSON 数据格式
      • XML 数据格式
      • 用 Gob 传输数据
      • Go 中的密码学
    • 第13章:错误处理与测试
      • 错误处理
      • 运行时异常和 panic
      • 从 panic 中恢复(Recover)
      • 自定义包中的错误处理和 panicking
      • 一种用闭包处理错误的模式
      • 启动外部命令和程序
      • Go 中的单元测试和基准测试
      • 测试的具体例子
      • 用(测试数据)表驱动测试
      • 性能调试:分析并优化 Go 程序
    • 第14章:协程(goroutine)与通道(channel)
      • 并发、并行和协程
      • 协程间的信道
      • 协程的同步:关闭通道-测试阻塞的通道
      • 使用 select 切换协程
      • 通道、超时和计时器(Ticker)
      • 协程和恢复(recover)
      • 新旧模型对比:任务和worker
      • 惰性生成器的实现
      • 实现 Futures 模式
      • 复用
      • 限制同时处理的请求数
      • 链式协程
      • 在多核心上并行计算
      • 并行化大量数据的计算
      • 漏桶算法
      • 对Go协程进行基准测试
      • 使用通道并发访问对象
    • 第15章:网络、模版与网页应用
      • tcp 服务器
      • 一个简单的 web 服务器
      • 访问并读取页面数据
      • 写一个简单的网页应用
      • 确保网页应用健壮
      • 用模板编写网页应用
      • 探索 template 包
      • 精巧的多功能网页服务器
      • 用 rpc 实现远程过程调用
      • 基于网络的通道 netchan
      • 与 websocket 通信
      • 用 smtp 发送邮件
  • 第四部分:实际应用
    • 第16章:常见的陷阱与错误
      • 误用短声明导致变量覆盖
      • 误用字符串
      • 发生错误时使用 defer 关闭一个文件
      • 何时使用new()和make()
      • 不需要将一个指向切片的指针传递给函数
      • 使用指针指向接口类型
      • 使用值类型时误用指针
      • 误用协程和通道
      • 闭包和协程的使用
      • 糟糕的错误处理
    • 第17章:模式
      • 逗号 ok 模式
      • defer 模式
      • 可见性模式
      • 运算符模式和接口
    • 第18章:出于性能考虑的实用代码片段
      • 字符串
      • 数组和切片
      • 映射
      • 结构体
      • 接口
      • 函数
      • 文件
      • 协程(goroutine)与通道(channel)
      • 网络和网页应用
      • 其他
      • 出于性能考虑的最佳实践和建议
    • 第19章:构建一个完整的应用程序
      • 简介
      • 短网址项目简介
      • 数据结构
      • 用户界面:web 服务端
      • 持久化存储:gob
      • 用协程优化性能
      • 以 json 格式存储
      • 多服务器处理架构
      • 使用代理缓存
      • 总结和增强
    • 第 20 章:Go 语言在 Google App Engine 的使用
      • 什么是 Google App Engine?
      • 云上的 Go
      • 安装 Go App Engine SDK:为 Go 部署的开发环境
      • 建造你自己的 Hello world 应用
      • 使用用户服务和探索其 API
      • 处理窗口
      • 使用数据存储
      • 上传到云端
    • 第 21 章:真实世界中 Go 的使用
      • Heroku:一个使用 Go 的高度可用一致数据存储
      • MROffice:一个使用 Go 的呼叫中心网络电话 (eBook/VOIP) 系统
      • Atlassian:一个虚拟机群管理系统
      • Camilistore:一个可寻址内容存储系统
      • Go 语言的其他应用
  • 附录
    • 其他
      • 关于本文16.10.2小结糟糕错误处理的一些见解
    • A 代码引用
    • B 有趣的 Go 引用
    • C 代码示例列表
      • 目录
      • 第2章示例
      • 第3章示例
      • 第4章示例
      • 第5章示例
      • 第6章示例
      • 第7章示例
      • 第8章示例
      • 第9章示例
      • 第10章示例
      • 第11章示例
      • 第12章示例
      • 第13章示例
      • 第14章示例
      • 第15章示例
      • 第16章示例
      • 第19章示例
      • 第20章示例
    • D 书中的包引用
    • E 书中的工具引用
    • F 常见问题解答
    • G 习题答案
      • 第4章答案
      • 第5章答案
      • 第6章答案
      • 第7章答案
      • 第8章答案
      • 第9章答案
      • 第10章答案
      • 第11章答案
      • 第12章答案
      • 第13章答案
      • 第14章答案
      • 第15章答案
      • 第16章答案
      • 第19章答案
      • 第20章答案
    • H 参考文献
Powered by GitBook
On this page
  • 10.6.1 方法是什么
  • 10.6.2 函数和方法的区别
  • 10.6.3 指针或值作为接收者
  • 10.6.4 方法和未导出字段
  • 10.6.5 内嵌类型的方法和继承
  • 10.6.6 如何在类型中嵌入功能
  • 10.6.7 多重继承
  • 10.6.8 通用方法和方法命名
  • 10.6.9 和其他面向对象语言比较 Go 的类型和方法
  • 链接

Was this helpful?

  1. 第二部分:语言的核心结构与技术
  2. 第10章:结构(struct)与方法(method)

方法

Previous匿名字段和内嵌结构体Next类型的 String() 方法和格式化描述符

Last updated 2 years ago

Was this helpful?

10.6.1 方法是什么

在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者 (receiver) 上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型(参考),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type...。

最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。

类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集 (method set)。

因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix

别名类型没有原始类型上已经定义过的方法。

定义方法的一般格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

在方法名之前,func 关键字之后的括号中指定 receiver。

如果 recv 是 receiver 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()。

如果 recv 是一个指针,Go 会自动解引用。

如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:

func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }

recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:

package main

import "fmt"

type TwoInts struct {
	a int
	b int
}

func main() {
	two1 := new(TwoInts)
	two1.a = 12
	two1.b = 10

	fmt.Printf("The sum is: %d\n", two1.AddThem())
	fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))

	two2 := TwoInts{3, 4}
	fmt.Printf("The sum is: %d\n", two2.AddThem())
}

func (tn *TwoInts) AddThem() int {
	return tn.a + tn.b
}

func (tn *TwoInts) AddToParam(param int) int {
	return tn.a + tn.b + param
}

输出:

The sum is: 22
Add them to the param: 42
The sum is: 7

下面是非结构体类型上方法的例子:

package main

import "fmt"

type IntVector []int

func (v IntVector) Sum() (s int) {
	for _, x := range v {
		s += x
	}
	return
}

func main() {
	fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
}

定义结构体 employee,它有一个 salary 字段,给这个结构体定义一个方法 giveRaise 来按照指定的百分比增加薪水。

下面这段代码有什么错?

package main

import "container/list"

func (p *list.List) Iter() {
	// ...
}

func main() {
	lst := new(list.List)
	for _= range lst.Iter() {
	}
}

类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float32(64) 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误:

cannot define new methods on non-local type int

比如想在 time.Time 上定义如下方法:

func (t time.Time) first3Chars() string {
	return time.LocalTime().String()[0:3]
}

类型在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。

但是有一个间接的方式:可以先定义该类型(比如:int 或 float32(64))的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。

package main

import (
	"fmt"
	"time"
)

type myTime struct {
	time.Time //anonymous field
}

func (t myTime) first3Chars() string {
	return t.Time.String()[0:3]
}
func main() {
	m := myTime{time.Now()}
	// 调用匿名 Time 上的 String 方法
	fmt.Println("Full time now:", m.String())
	// 调用 myTime.first3Chars
	fmt.Println("First 3 chars:", m.first3Chars())
}

/* Output:
Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011
First 3 chars: Mon
*/

10.6.2 函数和方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。

不要忘记 Method1() 后边的括号 (),否则会引发编译器错误:method recv.Method1 is not an expression, must be called

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。

方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

10.6.3 指针或值作为接收者

鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。

如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

下面的例子 pointer_value.go 作了说明:change()接受一个指向 B 的指针,并改变它内部的成员;write() 通过拷贝接受 B 的值并只输出 B 的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在指针上调用方法,Go 替我们做了这些事情。b1 是值而 b2 是指针,方法都支持运行了。

package main

import (
	"fmt"
)

type B struct {
	thing int
}

func (b *B) change() { b.thing = 1 }

func (b B) write() string { return fmt.Sprint(b) }

func main() {
	var b1 B // b1 是值
	b1.change()
	fmt.Println(b1.write())

	b2 := new(B) // b2 是指针
	b2.change()
	fmt.Println(b2.write())
}

/* 输出:
{1}
{1}
*/

试着在 write() 中改变接收者 b 的值:将会看到它可以正常编译,但是开始的 b 没有被改变。

我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 Point3 的值来做计算:

type Point3 struct { x, y, z float64 }
// A method on Point3
func (p Point3) Abs() float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}

这样做稍微有点昂贵,因为 Point3 是作为值传递给方法的,因此传递的是它的拷贝,这在 Go 中是合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。

假设 p3 定义为一个指针:p3 := &Point{ 3, 4, 5}。

可以使用 p3.Abs() 来替代 (*p3).Abs()。

因此 two2.AddThem 可以替代 (&two2).AddThem()。

在值和指针上调用方法:

可以有连接到类型的方法,也可以有连接到类型指针的方法。

但是这没关系:对于类型 T,如果在 \*T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()。

指针方法和值方法都可以在指针或非指针上被调用,如下面程序所示,类型 List 在值上有一个方法 Len(),在指针上有一个方法 Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。

package main

import (
	"fmt"
)

type List []int

func (l List) Len() int        { return len(l) }
func (l *List) Append(val int) { *l = append(*l, val) }

func main() {
	// 值
	var lst List
	lst.Append(1)
	fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1)

	// 指针
	plst := new(List)
	plst.Append(2)
	fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1)
}

10.6.4 方法和未导出字段

考虑 person2.go 中的 person 包:类型 Person 被明确的导出了,但是它的字段没有被导出。例如在 use_person2.go 中 p.firstName 就是错误的。该如何在另一个程序中修改或者只是读取一个 Person 的名字呢?

这可以通过面向对象语言一个众所周知的技术来完成:提供 getter() 和 setter() 方法。对于 setter() 方法使用 Set... 前缀,对于 getter() 方法只使用成员名。

package person

type Person struct {
	firstName string
	lastName  string
}

func (p *Person) FirstName() string {
	return p.firstName
}

func (p *Person) SetFirstName(newName string) {
	p.firstName = newName
}
package main

import (
	"./person"
	"fmt"
)

func main() {
	p := new(person.Person)
	// p.firstName undefined
	// (cannot refer to unexported field or method firstName)
	// p.firstName = "Eric"
	p.SetFirstName("Eric")
	fmt.Println(p.FirstName()) // Output: Eric
}

并发访问对象

10.6.5 内嵌类型的方法和继承

当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入 (mixin)。

下面是一个示例(可以在练习 10.8 中进一步学习):假定有一个 Engine 接口类型,一个 Car 结构体类型,它包含一个 Engine 类型的匿名字段:

type Engine interface {
	Start()
	Stop()
}

type Car struct {
	Engine
}

我们可以构建如下的代码:

func (c *Car) GoToWorkIn() {
	// get in car
	c.Start()
	// drive to work
	c.Stop()
	// get out of car
}
package main

import (
	"fmt"
	"math"
)

type Point struct {
	x, y float64
}

func (p *Point) Abs() float64 {
	return math.Sqrt(p.x*p.x + p.y*p.y)
}

type NamedPoint struct {
	Point
	name string
}

func main() {
	n := &NamedPoint{Point{3, 4}, "Pythagoras"}
	fmt.Println(n.Abs()) // 打印 5
}

内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法。

可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。

func (n *NamedPoint) Abs() float64 {
	return n.Point.Abs() * 100.
}

现在 fmt.Println(n.Abs()) 会打印 500。

结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。

创建一个上面 Car 和 Engine 可运行的例子,并且给 Car 类型一个 wheelCount 字段和一个 numberOfWheels() 方法。

创建一个 Mercedes 类型,它内嵌 Car,并新建 Mercedes 的一个实例,然后调用它的方法。

然后仅在 Mercedes 类型上创建方法 sayHiToMerkel() 并调用它。

10.6.6 如何在类型中嵌入功能

主要有两种方法来实现在类型中嵌入功能:

A:聚合(或组合):包含一个所需功能类型的具名字段。

B:内嵌:内嵌(匿名地)所需功能类型,像前一节 10.6.5 所演示的那样。

为了使这些概念具体化,假设有一个 Customer 类型,我们想让它通过 Log 类型来包含日志功能,Log 类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的 Log 类型,然后将它作为特定类型的一个字段,并提供 Log(),它返回这个日志的引用。

package main

import (
	"fmt"
)

type Log struct {
	msg string
}

type Customer struct {
	Name string
	log  *Log
}

func main() {
	c := new(Customer)
	c.Name = "Barak Obama"
	c.log = new(Log)
	c.log.msg = "1 - Yes we can!"
	// shorter
	c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}}
	// fmt.Println(c) &{Barak Obama 1 - Yes we can!}
	c.Log().Add("2 - After me the world will be a better place!")
	//fmt.Println(c.log)
	fmt.Println(c.Log())

}

func (l *Log) Add(s string) {
	l.msg += "\n" + s
}

func (l *Log) String() string {
	return l.msg
}

func (c *Customer) Log() *Log {
	return c.log
}

输出:

1 - Yes we can!
2 - After me the world will be a better place!
package main

import (
	"fmt"
)

type Log struct {
	msg string
}

type Customer struct {
	Name string
	Log
}

func main() {
	c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}}
	c.Add("2 - After me the world will be a better place!")
	fmt.Println(c)

}

func (l *Log) Add(s string) {
	l.msg += "\n" + s
}

func (l *Log) String() string {
	return l.msg
}

func (c *Customer) String() string {
	return c.Name + "\nLog:" + fmt.Sprintln(c.Log.String())
}

输出:

Barak Obama
Log:1 - Yes we can!
2 - After me the world will be a better place!

内嵌的类型不需要指针,Customer 也不需要 Add 方法,它使用 Log 的 Add 方法,Customer 有自己的 String 方法,并且在它里面调用了 Log 的 String 方法。

如果内嵌类型嵌入了其他类型,也是可以的,那些类型的方法可以直接在外层类型中使用。

因此一个好的策略是创建一些小的、可复用的类型作为一个工具箱,用于组成域类型。

10.6.7 多重继承

多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。

作为一个例子,假设有一个类型 CameraPhone,通过它可以 Call(),也可以 TakeAPicture(),但是第一个方法属于类型 Phone,第二个方法属于类型 Camera。

package main

import (
	"fmt"
)

type Camera struct{}

func (c *Camera) TakeAPicture() string {
	return "Click"
}

type Phone struct{}

func (p *Phone) Call() string {
	return "Ring Ring"
}

type CameraPhone struct {
	Camera
	Phone
}

func main() {
	cp := new(CameraPhone)
	fmt.Println("Our new CameraPhone exhibits multiple behaviors...")
	fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture())
	fmt.Println("It works like a Phone too: ", cp.Call())
}

输出:

Our new CameraPhone exhibits multiple behaviors...
It exhibits behavior of a Camera: Click
It works like a Phone too: Ring Ring

定义一个结构体类型 Base,它包含一个字段 id,方法 Id() 返回 id,方法 SetId() 修改 id。结构体类型 Person 包含 Base,及 FirstName 和 LastName 字段。结构体类型 Employee 包含一个 Person 和 salary 字段。

创建一个 employee 实例,然后显示它的 id。

首先预测一下下面程序的结果,然后动手实验下:

package main

import (
	"fmt"
)

type Base struct{}

func (Base) Magic() {
	fmt.Println("base magic")
}

func (self Base) MoreMagic() {
	self.Magic()
	self.Magic()
}

type Voodoo struct {
	Base
}

func (Voodoo) Magic() {
	fmt.Println("voodoo magic")
}

func main() {
	v := new(Voodoo)
	v.Magic()
	v.MoreMagic()
}

10.6.8 通用方法和方法命名

10.6.9 和其他面向对象语言比较 Go 的类型和方法

在如 C++、Java、C# 和 Ruby 这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。

在 Go 语言中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go 具有更大的灵活性。

下面的模式就很好的说明了这个问题:

Go 不需要一个显式的类定义,如同 Java、C++、C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。

比如:我们想定义自己的 Integer 类型,并添加一些类似转换成字符串的方法,在 Go 中可以如下定义:

type Integer int
func (i *Integer) String() string {
    return strconv.Itoa(int(*i))
}

在 Java 或 C# 中,这个方法需要和类 Integer 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。

总结

在 Go 中,类型就是类(数据和关联的方法)。Go 不知道类似面向对象语言的类继承的概念。继承有两个好处:代码复用和多态。

在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 组件编程 (Component Programming)。

许多开发者说相比于类继承,Go 的接口提供了更强大、却更简单的多态行为。

备注

问题 10.1

我们在某个类型的变量上使用点号调用一个方法:variable.method(),在使用 Go 以前,在哪儿碰到过面向对象的点号?

问题 10.2

a)假设定义: type Integer int,完成 get() 方法的方法体: func (p Integer) get() int { ... }。

b)定义: func f(i int) {}; var v Integer ,如何就 v 作为参数调用f?

c)假设 Integer 定义为 type Integer struct {n int},完成 get() 方法的方法体:func (p Integer) get() int { ... }。

d)对于新定义的 Integer,和 b)中同样的问题。

链接

示例 10.10 :

示例 10.11 :

练习 10.6

练习 10.7

示例 10.12 :

示例 10.13 :

像例子 10.10 () 中接收者类型是 *TwoInts 的方法 AddThem(),它能在类型 TwoInts 的值上被调用,这是自动间接发生的。

示例 10.14 :

示例 10.15 :

示例 10.16 :

对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 sync(参考中的方法。在中我们会通过 goroutines 和 channels 探索另一种方式。

下面是 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:

在示例 10.18 中添加:

因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承,就像:type Child struct { Father; Mother}。在中会进一步讨论这个问题。

练习 10.8

方式 A 可以通过如下方法实现(使用了中的 String() 功能):

示例 10.19 :

相对的方式 B 可能会像这样 ():

只要嵌入这两个类型就可以解决这个问题,如下所示 ():

练习 10.9 :

从 point.go 开始(的练习):使用方法来实现 Abs() 和 Scale()函数,Point 作为方法的接收者类型。也为 Point3 和 Polar 实现 Abs() 方法。完成了 point.go 中同样的事情,只是这次通过方法。

练习 10.10 :

练习 10.11 :

在编程中一些基本操作会一遍又一遍的出现,比如打开 (Open)、关闭 (Close)、读 (Read)、写 (Write)、排序(Sort) 等等,并且它们都有一个大致的意思:打开 (Open)可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。在 Go 语言中,通过使用接口(参考),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 Open()、Read()、Write()等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 convert-to-string() 方法,应该命名为 String(),而不是 ToString()(参考)。

如果真的需要更多面向对象的能力,看一下 包 (Go Object-Oriented Programming),它由 Scott Pakin 编写: 它给 Go 提供了 JavaScript 风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。

上一节:

下一节:

第 11 章
method1 .go
method2.go
employee_salary.go
iteration_list.go
method_on_time.go
pointer_value.go
method1.go
methodset1.go
person2.go
use_person2.go
第 9.3 节
第 14.17 节
method3.go
method4.go
第 10.6.7 节
inheritance_car.go
第 10.7 节
embed_func1.go
embed_func2.go
mult_inheritance.go
point_methods.go
第 10.1 节
inherit_methods.go
magic.go
第 11 章
第 10.7 节
goop
目录
匿名字段和内嵌结构体
类型的 String() 方法和格式化描述符