目录
工厂模式
概念
由于 Go 中缺少类和继承等 OOP 特性, 所以无法使用 Go 来实现经典的工厂方法模式。 不过, 我们仍然能实现模式的基础版本, 即简单工厂。
在本例中, 我们将使用工厂结构体来构建多种类型的武器。
首先, 我们来创建一个名为 iGun
的接口, 其中将定义一支枪所需具备的所有方法。 然后是实现了 iGun 接口的 gun
枪支结构体类型。 两种具体的枪支—— ak47
与 musket
火枪 ——两者都嵌入了枪支结构体, 且间接实现了所有的 iGun
方法。
gunFactory
枪支工厂结构体将发挥工厂的作用, 即通过传入参数构建所需类型的枪支。 main.go 则扮演着客户端的角色。 其不会直接与 ak47
或 musket
进行互动, 而是依靠 gunFactory
来创建多种枪支的实例, 仅使用字符参数来控制生产。
示例
iGun.go: 产品接口
package main
type iGun interface {
setName(name string)
setPower(power int)
getName() string
getPower() int
}
gun.go: 具体产品
package main
type gun struct {
name string
power int
}
func (g *gun) setName(name string) {
g.name = name
}
func (g *gun) getName() string {
return g.name
}
func (g *gun) setPower(power int) {
g.power = power
}
func (g *gun) getPower() int {
return g.power
}
ak47.go: 具体产品
package main
type ak47 struct {
gun
}
func newAk47() iGun {
return &ak47{
gun: gun{
name: "AK47 gun",
power: 4,
},
}
}
musket.go: 具体产品
package main
type musket struct {
gun
}
func newMusket() iGun {
return &musket{
gun: gun{
name: "Musket gun",
power: 1,
},
}
}
gunFactory.go: 工厂
package main
import "fmt"
func getGun(gunType string) (iGun, error) {
if gunType == "ak47" {
return newAk47(), nil
}
if gunType == "musket" {
return newMusket(), nil
}
return nil, fmt.Errorf("Wrong gun type passed")
}
main.go: 客户端代码
package main
import "fmt"
func main() {
ak47, _ := getGun("ak47")
musket, _ := getGun("musket")
printDetails(ak47)
printDetails(musket)
}
func printDetails(g iGun) {
fmt.Printf("Gun: %s", g.getName())
fmt.Println()
fmt.Printf("Power: %d", g.getPower())
fmt.Println()
}
output.txt: 执行结果
Gun: AK47 gun
Power: 4
Gun: Musket gun
Power: 1
接口
此部分来源于Go 语言接口的原理 | Go 语言设计与实现
隐式接口
很多面向对象语言都有接口这一概念,例如 Java 和 C#。Java 的接口不仅可以定义方法签名,还可以定义变量,这些定义的变量可以直接在实现接口的类中使用,这里简单介绍一下 Java 中的接口:
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
上述代码定义了一个必须实现的方法 sayHello
和一个会注入到实现类的变量 hello
。在下面的代码中,MyInterfaceImpl
实现了 MyInterface
接口:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。首先,我们简单了解一下在 Go 语言中如何定义接口。定义接口需要使用 interface
关键字,在接口中我们只能定义方法签名,不能包含成员变量,一个常见的 Go 语言接口是这样的:
type error interface {
Error() string
}
如果一个类型需要实现 error
接口,那么它只需要实现 Error() string
方法,下面的 RPCError
结构体就是 error
接口的一个实现:
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
细心的读者可能会发现上述代码根本就没有 error
接口的影子,这是为什么呢?Go 语言中接口的实现都是隐式的,我们只需要实现 Error() string
方法就实现了 error
接口。Go 语言实现接口的方式与 Java 完全不同:
-
在 Java 中:实现接口需要显式地声明接口并实现所有方法;
-
在 Go 中:实现接口的所有方法就隐式地实现了接口;
我们使用上述 RPCError
结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:
func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}
func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}
func AsErr(err error) error {
return err
}
Go 语言在编译期间对代码进行类型检查,上述代码总共触发了三次类型检查:
-
将
*RPCError
类型的变量赋值给error
类型的变量rpcErr
; -
将
*RPCError
类型的变量rpcErr
传递给签名中参数类型为error
的AsErr
函数; -
将
*RPCError
类型的变量从函数签名的返回值类型为error
的NewRPCError
函数中返回;
从类型检查的过程来看,编译器仅在需要时才检查类型,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。
类型
接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}
:
图 4-7 Go 语言中的两种接口
Go 语言使用 runtime.iface 表示第一种接口,使用 runtime.eface 表示第二种不包含任何方法的接口 interface{}
,两种接口虽然都使用 interface
声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。
需要注意的是,与 C 语言中的 void *
不同,interface{}
类型不是任意类型。如果我们将类型转换成了 interface{}
类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}
。
package main
func main() {
type Test struct{}
v := Test{}
Print(v)
}
func Print(v interface{}) {
println(v)
}
上述函数不接受任意类型的参数,只接受 interface{}
类型的值,在调用 Print
函数时会对参数 v
进行类型转换,将原来的 Test
类型转换成 interface{}
类型,本节会在后面介绍类型转换的实现原理。
指针和接口
在 Go 语言中同时使用指针和接口时会发生一些让人困惑的问题,接口在定义一组方法时没有对实现的接收者做限制,所以我们会看到某个类型实现接口的两种方式:
图 4-8 结构体和指针实现接口
这是因为结构体类型和指针类型是不同的,就像我们不能向一个接受指针的函数传递结构体一样,在实现接口时这两种类型也不能划等号。虽然两种类型不同,但是上图中的两种实现不可以同时存在,Go 语言的编译器会在结构体类型和指针类型都实现一个方法时报错 “method redeclared”。
对 Cat
结构体来说,它在实现接口时可以选择接受者的类型,即结构体或者结构体指针,在初始化时也可以初始化成结构体或者指针。下面的代码总结了如何使用结构体、结构体指针实现接口,以及如何使用结构体、结构体指针初始化变量。
type Cat struct {}
type Duck interface { ... }
func (c Cat) Quack {} // 使用结构体实现接口
func (c *Cat) Quack {} // 使用结构体指针实现接口
var d Duck = Cat{} // 使用结构体初始化变量
var d Duck = &Cat{} // 使用结构体指针初始化变量
实现接口的类型和初始化返回的类型两个维度共组成了四种情况,然而这四种情况不是都能通过编译器的检查:
结构体实现接口 | 结构体指针实现接口 | |
---|---|---|
结构体初始化变量 | 通过 | 不通过 |
结构体指针初始化变量 | 通过 | 通过 |
四种中只有使用指针实现接口,使用结构体初始化变量无法通过编译,其他的三种情况都可以正常执行。当实现接口的类型和初始化变量时返回的类型时相同时,代码通过编译是理所应当的:
-
方法接受者和初始化类型都是结构体;
-
方法接受者和初始化类型都是结构体指针;
而剩下的两种方式为什么一种能够通过编译,另一种无法通过编译呢?我们先来看一下能够通过编译的情况,即方法的接受者是结构体,而初始化的变量是结构体指针:
type Cat struct{}
func (c Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = &Cat{}
c.Quack()
}
作为指针的 &Cat{}
变量能够隐式地获取到指向的结构体,所以能在结构体上调用 Walk
和 Quack
方法。我们可以将这里的调用理解成 C 语言中的 d->Walk()
和 d->Speak()
,它们都会先获取指向的结构体再执行对应的方法。
但是如果我们将上述代码中方法的接受者和初始化的类型进行交换,代码就无法通过编译了:
type Duck interface {
Quack()
}
type Cat struct{}
func (c *Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = Cat{}
c.Quack()
}
$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
Cat does not implement Duck (Quack method has pointer receiver)
编译器会提醒我们:Cat
类型没有实现 Duck
接口,Quack
方法的接受者是指针。这两个报错对于刚刚接触 Go 语言的开发者比较难以理解,如果我们想要搞清楚这个问题,首先要知道 Go 语言在传递参数时都是传值的。
图 4-9 实现接口的接受者类型
如上图所示,无论上述代码中初始化的变量 c
是 Cat{}
还是 &Cat{}
,使用 c.Quack()
调用方法时都会发生值拷贝:
-
如上图左侧,对于
&Cat{}
来说,这意味着拷贝一个新的&Cat{}
指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体; -
如上图右侧,对于
Cat{}
来说,这意味着Quack
方法会接受一个全新的Cat{}
,因为方法的参数是*Cat
,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要,在这里我们只想解释现象背后的原因。
nil 和 non-nil
我们可以通过一个例子理解Go 语言的接口类型不是任意类型这一句话,下面的代码在 main
函数中初始化了一个 *TestStruct
类型的变量,由于指针的零值是 nil
,所以变量 s
在初始化之后也是 nil
:
package main
type TestStruct struct{}
func NilOrNot(v interface{}) bool {
return v == nil
}
func main() {
var s *TestStruct
fmt.Println(s == nil) // #=> true
fmt.Println(NilOrNot(s)) // #=> false
}
$ go run main.go
true
false
我们简单总结一下上述代码执行的结果:
-
将上述变量与
nil
比较会返回true
; -
将上述变量传入
NilOrNot
方法并与nil
比较会返回false
;
出现上述现象的原因是 —— 调用 NilOrNot
函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*TestStruct
类型会转换成 interface{}
类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 TestStruct
,所以转换后的变量与 nil
不相等。
面向对象编程思想
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
面向对象编程三大特性
封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
封装的优点
-
隐藏实现细节
-
可以对数据进行验证
如何体现封装
-
对结构体中的属性进行封装
-
通过方法,包 实现封装
封装的实现步骤
-
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) { //加入数据验证的业务逻辑var.字段 = 参数 }
-
提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() { return var.age; }
继承
-
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体(primarySchoolStudents,collegeStudents)中抽象出结构体(比如Student),在该结构体中定义这些相同的属性和方法。
-
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。
-
在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
详细说明
-
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法, 都可以使用。
-
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
-
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
-
上面一条也就是多重继承
-
如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
-
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。