go 究竟是不是面向对象语言,官方给出了这样的回答:
可以认为是,也可以认为否。虽然Go有类型和方法,并且允许面向对象的编程风格,但在Go中没有继承。Go中的接口提供了一种不同的方法,易于使用,在某些方面更为通用。此外,Go中缺少继承之类的层次结构,使得Go中的“对象”比C++或java语言中的对象更加轻量级。
我们熟知的面向对象的语言有 Java。在Java 中,头等公民是对象,一切都要封装在一个对象中。而在Go语言中,函数是头等公民,函数可以单独存在,可以作为参数或者返回值进行传递。
go封装数据和方法
数据封装
go 语言中,可以把 struct 当作 Java 中的类,使用 struct 封装数据。
结构体定义,比如如下的定义相当于是封装了一个 Employee 对象。
type Employee struct {
Id string
Name string
Age int
}
实例创建及初始化
e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := new(Employee) //注意这⾥返回的引⽤(指针),相当于 e := &Employee{}
e2.Id = “2" //与其他主要编程语⾔的差异:通过实例的指针访问成员不需要使⽤->
e2.Age = 22
e2.Name = “Rose
示例
type Employee struct {
Id string
Name string
Age int
}
func Test(t *testing.T) {
e0 := Employee{Id: "1000", Name: "Lily", Age: 20}
e1 := Employee{"1001", "Bob", 20}
e2 := new(Employee) //返回指针
// 虽然e2是指针,但访问field时,不需要使用 -> 符号,直接用.符号就可以进行访问
e2.Id = "1002"
e2.Name = "Simon"
e2.Age = 22
t.Log("e0", e0)
t.Log("e1", e1)
t.Log("e2", e2)
// 返回 e1 和 e2的创建类型
t.Logf("e1 is %T", e1)
t.Logf("e2 is %T", e2)
}
输出
=== RUN Test
object_oriented_test.go:19: e0 {1000 Lily 20}
object_oriented_test.go:20: e1 {1001 Bob 20}
object_oriented_test.go:21: e2 &{1002 Simon 22}
object_oriented_test.go:23: e1 is object_oriented.Employee
object_oriented_test.go:24: e2 is *object_oriented.Employee
--- PASS: Test (0.00s)
PASS
成员方法
定义对象的成员方法主要有如下两种方法。需要注意的是,在Java 中成员方法都是定义在一个类中的,但是go语言中,定义方法并不是在 struct 中进行定义,只需新建一个方法,传入的 什么类型的指针,就表示这个方法归属于什么指针类型的对象。
type Employee struct {
Id string
Name string
Age int
}
//第⼀种定义⽅式在实例对应⽅法被调⽤时,实例的成员会进⾏值复制
func (e Employee) String() string {
return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}
//第二种方法,复制的仅仅是指针,指针指向的对象不会发生复制。通常情况下为了避免内存拷⻉我们使⽤第⼆种定义⽅式
func (e *Employee) String() string {
return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}
示例
type Employee struct {
Id string
Name string
Age int
}
//func (e Employee) String() string {
// return fmt.Sprintf("ID:%s Name:%s Age:%d", e.Id, e.Name, e.Age)
//}
// 成员方法的写法 (e *Employee) 对应着 Employee 这个对象,使用这种方法,不用发生值复制
func (e *Employee) String() string {
// %x 打印地址
fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
fmt.Println()
return fmt.Sprintf("ID:%s Name:%s Age:%d", e.Id, e.Name, e.Age)
}
func TestObjectMethod(t *testing.T) {
e := Employee{Id: "1000", Name: "Lily", Age: 20}
fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
fmt.println()
fmt.Println(e.String())
}
从输出中可以发现使用 e *Employee 时,Employee 对象的name的地址都是同样的,说明参数传递时,没有发生值复制。
=== RUN TestObjectMethod
Address is c00006e5b0
Address is c00006e5b0
ID:1000 Name:Lily Age:20
--- PASS: TestObjectMethod (0.00s)
PASS
go语言的接口
接口的定义与使用
go 语言中没有 implements 这样的关键字来显示声明某个类实现了某个接口。在 go 语言中只要定义的方法签名与接口中定义的方法的方法签名相同,且传入入了某个类型的对象,那么就可以认为这个对象所在的类 implements 了某个接口。
这就是所谓的鸭子理论:一个东西看起来像鸭子,走起来也像鸭子,叫起来也像鸭子,那我们就认为这个东西就是个鸭子。
示例
import "testing"
// 定义接口
type Programmer interface {
// 需要注意的是必须实现 interface 中的所有方法,才会有继承关系
WriteHelloWorld()
}
// 定义实现类
type GoProgrammer struct {
}
// 没有显示指定某个类 implement某个接口,新建一个方法与 interface 中的方法签名相同
// 且传入相应的 struct ,即可认为实现了某个接口中的方法
func (p *GoProgrammer) WriteHelloWorld() {
fmt.Println("Hello World")
}
func TestInterface(t *testing.T) {
var p Programmer
p = new(GoProgrammer)
p.WriteHelloWorld()
}
我的开发环境是 Goland。
可以发现,当具有继承关系时,IDE 中会出现一些向上向下的箭头
如下图所示,当我在接口中再定义一个方法,却没有实现该方法,此时 WriteHelloWorld 这个方法归属于 GoProgrammer 这个对象,并没有实现 Programmer 这个接口,可以发现 IDE 中也没有提示任何的继承关系。
接口定义好,实现类也有的了,就可以这样调用了。
func TestInterface(t *testing.T) {
var p Programmer
p = new(GoProgrammer)
p.WriteHelloWorld()
}
输出
=== RUN TestInterface
Hello World
--- PASS: TestInterface (0.00s)
PASS
综合上面的示例,我们可以看出 go 语言中的接口更加灵活。接口为非入侵性,实现不依赖于接口定义。所以接口的定义可以包含在接口使用者(客户端)包内,不用放在实现类的包中。
在 Java 中使用接口,可以达到定义与实现分离的目的。这样,使用者就可以为接口选择不同的实现。但在开发的过程中,我们会发现,Java 中定义的接口与实现类通常都是放在一个包下的,通常都将接口的定义与实现都打包放在服务端的代码中,这在一定程度上造成了耦合。
List<String> list1 = new ArrayList<>();
List<String> list2 = new LinkedList<>();
List<String> list3 = new CopyOnWriteArrayList<>();
自定义类型(取别名)
在 Go 语言中,函数可以作为参数也可以作为返回值,在方法中定义的时候,函数的签名太长,定义的方法就看起来不美观,这个时候就可以使用自定义的类型。
// 自定义类型
type InnerFuncType func(param int) int
// 代码1 代码1的方法定义等效于方法2的定义
func calculateSpendTime(innerFunc InnerFuncType) InnerFuncType {
return func(param int) int {
startTime := time.Now()
// 计算返回值
ret := innerFunc(param)
fmt.Printf("spend time = %f", time.Since(startTime).Seconds())
fmt.Println()
return ret
}
}
// 代码2
//func calculateSpendTime(innerFunc func(param int) int) func(param int) int{
// return func(param int) int {
// startTime := time.Now()
// // 计算返回值
// ret := innerFunc(param)
// fmt.Printf("spend time = %f", time.Since(startTime).Seconds())
// fmt.Println()
// return ret
// }
//}
go 接口的最佳实践
- 倾向于使用小的接口定义,接口中尽量只定义一个方法
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
- 较大的接口尽量由较小的接口组合而成
type ReadWriter interface {
Reader
Writer
}
- 只依赖于必要功能的最小接口(便于复用)
比如仅仅只需要 Reader 中的 Read 方法,那么传入的参数使用 Reader 接口即可,不要使用 ReadWriter 接口
func StoreData(reader Reader) error {
…
}
通过组合实现扩展
组合
go 中没有继承,不能通过继承实现扩展,可以通过组合的方式实现扩展。
// 定义了 Pet
type Pet struct {
}
// Pet 的成员方法
func (p *Pet) Speak(voice string) {
fmt.Println(voice)
}
func (p *Pet) SpeakToHost(voice string, host string) {
p.Speak(voice)
fmt.Println(host)
}
// 定义了 Dog
type Dog struct {
// 定义了Pet类型的指针
p *Pet
}
func TestExtension(t *testing.T) {
d := &Dog{}
// 通过使用 Pet 类型的指针就能调用到 Speak 方法
d.p.Speak("Wang Wang Wang")
d.p.SpeakToHost("Wang Wang Wang", "Simon")
}
输出
=== RUN TestExtension
Wang Wang Wang
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS
匿名类型组合
在进行组合的时候,Go 提供了一个简化机制,不需要在定义结构体时再定义需要组合的对象的指针,如下所示:
// 定义了 Dog
type Dog struct {
// 在这里只需要写上需要组合的对象
Pet
}
然后调用的时候,就不用通过定义到 指针来进行调用了。
func TestExtension(t *testing.T) {
d := &Dog{}
d.Speak("Wang Wang Wang")
d.SpeakToHost("Wang Wang Wang", "Simon")
}
完整代码如下所示:
// 定义了 Pet
type Pet struct {
}
func (p *Pet) Speak(voice string) {
fmt.Println(voice)
}
func (p *Pet) SpeakToHost(voice string, host string) {
fmt.Println(voice, host)
}
// 定义了 Dog
type Dog struct {
// 在这里只需要写上需要组合的对象
Pet
}
func TestExtension(t *testing.T) {
d := &Dog{}
d.Speak("Wang Wang Wang")
d.SpeakToHost("Wang Wang Wang", "Simon")
}
输出与结构体中定义指针,然后通过指针调用方法的结果相同。
=== RUN TestExtension
Wang Wang Wang
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS
接着看,如果我们为 Dog 这个对象也增加了一个 Speak 这个方法,那么通过 d.Speak 进行调用时,会调用哪个方法呢?
func (d *Dog) Speak(voice string) {
fmt.Println("I am a dog ==>", voice)
}
完整代码
// 定义了 Pet
type Pet struct {
}
func (p *Pet) Speak(voice string) {
fmt.Println(voice)
}
func (p *Pet) SpeakToHost(voice string, host string) {
p.Speak(voice)
fmt.Println(host)
}
// 定义了 Dog
type Dog struct {
Pet
}
func (d *Dog) Speak(voice string) {
fmt.Println("I am a dog ==>", voice)
}
func TestExtension(t *testing.T) {
d := &Dog{}
fmt.Println("调用 Speak 方法")
d.Speak("Wang Wang Wang")
fmt.Println("调用 SpeakToHost 方法")
d.SpeakToHost("Wang Wang Wang", "Simon")
}
从调用 Speak 方法的输出中我们可以发现调用的是Dog类对应的 Speak 方法,而不是 Pet 类中的 Speak 方法,这点和 Java 继承中的 Override 很类似。但是再看调用 SpeakToHost 方法的结果可以发现,这又并不是 Java 中的继承,因为子类一旦重写了父类的某个方法,那么子类对这个方法的调用会替换为调用子类的方法。
=== RUN TestExtension
调用 Speak 方法
I am a dog ==> Wang Wang Wang
调用 SpeakToHost 方法
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS
上面的说法比较晦涩,可以写一段 Java 代码来看看。
定义 Pet 类
public class Pet {
public void Speak(String voice) {
System.out.println(voice);
}
public void speakToHost(String voice, String host) {
this.Speak(voice);
System.out.println(voice + " " + host);
}
}
定义 Dog 类
public class Dog extends Pet{
@Override
public void Speak(String voice) {
System.out.println("I am a dog I can " + voice);
}
}
测试类
public class Main {
public static void main(String[] args) {
Pet pet = new Pet();
System.out.println("pet 调用 Speak 方法");
pet.Speak("...");
System.out.println("pet 调用 SpeakToHost 方法");
pet.speakToHost("...", "Simon");
Pet dog = new Dog();
System.out.println("dog 调用 Speak 方法");
dog.Speak("Wang Wang Wang");
System.out.println("dog 调用 SpeakToHost 方法");
dog.speakToHost("Wang Wang Wang", "Simon");
}
}
输出,输出中可以发现子类有两个行为。子类没有 Override 的方法,就调用父类的方法,子类 Override 的方法,就调用子类 Override 的方法。
pet 调用 Speak 方法
...
pet 调用 SpeakToHost 方法
...
... Simon
dog 调用 Speak 方法
I am a dog I can Wang Wang Wang
dog 调用 SpeakToHost 方法
I am a dog I can Wang Wang Wang
Wang Wang Wang Simon
最后再总结一下 go 语言中的组合,虽然可以通过匿名类型组合的方式进行简写,某些地方与 Java 的继承相似,但是 go 中的组合并不是 Java 中的继承,不能使用 LSP(里氏替换原则,凡是可以用父类或者基类的地方,都可以用子类替换),也不存在子类,父类的说法,因此它们之间的引用并不能互相转换。
如下所示:即便进行了强制类型转换,也会报错,因此两者根本无法转换。
多态与空接口
多态
示例代码
package polymorphic
import (
"fmt"
"testing"
)
// 自定义类型 Code 实际上就是 string 类型
type Code string
// 定义接口
type Programmer interface {
// 需要注意的是必须实现 interface 中的所有方法
WriteHelloWorld() Code
}
// GoProgrammer
type GoProgrammer struct {
}
func (p *GoProgrammer) WriteHelloWorld() Code {
return "fmt.Println(\"Hello World\")"
}
// JavaProgrammer
type JavaProgrammer struct {
}
func (p *JavaProgrammer) WriteHelloWorld() Code {
return "System.out.println(\"Hello World\");"
}
// 传入的参数只能为引用类型
func writeFistProgram(p Programmer) {
// %T 输出实例或者是参数的类型
fmt.Printf("p的类型为:%T p的值为:%v\n", p, p.WriteHelloWorld())
}
// 多态
func TestPolymorphic(t *testing.T) {
// interface 不能实例化,对应的类型只能是一个指针
goProg := &GoProgrammer{} //等效为 goProg := new(GoProgrammer),不能写作 goProg := GoProgrammer{}
javaProg := &JavaProgrammer{} //等效为 javaProg := new(JavaProgrammer),不能写作 javaProg := JavaProgrammer{}
writeFistProgram(goProg)
writeFistProgram(javaProg)
}
输出
=== RUN TestPolymorphic
p的类型为:*polymorphic.GoProgrammer p的值为:fmt.Println("Hello World")
p的类型为:*polymorphic.JavaProgrammer p的值为:System.out.println("Hello World");
--- PASS: TestPolymorphic (0.00s)
PASS
空接口与断言
- 空接口可以表示任何类型(类似于 Java 中的 Object 类型)
- 通过空接口转化为具体的类型时,不是使用强制类型转换,而是通过断言。通过断言来将空接⼝转换为指定类型。
v, ok := p.(int) //ok=true 时为转换成功
func DoSomething(p interface{}) {
// 代码1 等效于代码2
// p.(int) 表示断言,如果能断言为 int,则 ok 为true
if i, ok := p.(int); ok {
fmt.Println("integer", i)
return
}
if s, ok := p.(string); ok {
fmt.Println("string", s)
return
}
fmt.Println("Unknown Type")
// 代码2
//switch t := p.(type) {
//case int:
// fmt.Println("integer", t)
//case string:
// fmt.Println("string", t)
//default:
// fmt.Println("Unknown Type")
//}
}
func TestEmptyInterface(t *testing.T) {
DoSomething("1000")
}
输出
=== RUN TestEmptyInterface
string 1000
--- PASS: TestEmptyInterface (0.00s)
PASS