Go语言实现面向对象非常简单,因为它省略了很多繁琐又没什么用的特性,但又不像C语言那样在设计之初就没考虑面向对象的问题导致实现的比较绕。这篇笔记我们简单介绍Go语言中面向对象的用法。
之前结构体章节我们已经学习过,Go语言没有类,但是在结构体上能够定义方法,这里我们再复习一下。
package main
import (
"math"
"fmt"
)
type Vector2 struct {
X float64
Y float64
}
func (v *Vector2) Abs() float64 {
return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
func main() {
v := &Vector2{1, 2}
fmt.Println(v.Abs())
}
其中v
就像一个参数,只不过它代表的是具有该方法的对象。至于为什么使用指针作为方法调用者参数,一般来说如果是复杂的自定义类型,是建议这么做的,这样可以防止对象拷贝的巨大开销。
下面例子定义了一个接口Vector
,其中定义了能够实现计算向量长度的方法,Vector2
和Vector3
实现了这个方法。
package main
import (
"math"
"fmt"
)
type Vector interface {
Abs() float64
}
type Vector3 struct {
X float64
Y float64
Z float64
}
type Vector2 struct {
X float64
Y float64
}
func (v *Vector2) Abs() float64 {
return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
func (v *Vector3) Abs() float64 {
return math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z)
}
func main() {
var v1 Vector
var v2 Vector
v1 = &Vector2{1, 2}
v2 = &Vector3{1, 2, 3}
fmt.Println(v1.Abs())
fmt.Println(v2.Abs())
}
这里向v1
和v2
赋值时,它们的类型我们声明为接口Vector
,实际上它是指针,而且默认值是nil
。
我们初始化的是接口的实现类Vector2
或Vector3
,这时必须加上取地址符,这和Java中不同,不要弄混了。如果声明v1
与v2
后没有为其赋值,就会导致空指针。此外,如果向函数传递Vector
类型的参数,它显然也是引用传递。
其实Go语言中,我们可以让编译器去进行自动类型推导:
v1 := &Vector2{1, 2}
这个类型推导是比较强大的,它会自动根据上下文决定变量的数据类型,因此我们就不用总是纠结应该定义为何种类型了。
Java中,所有对象的基类是Object
,该类实现了toString()
方法用于打印对象。Go语言中也有类似功能,但是设计的稍有不同。Go语言中,结构体可以实现Stringer
接口的String()
方法,实现自身打印输出,fmt
会自动调用该方法格式化输出变量。
package main
import (
"math"
"fmt"
)
type Person struct {
name string
age int
}
func (p *Person) String() string {
return fmt.Sprintf("%v / %v", p.name, p.age)
}
func main() {
p := &Person{"Tom", 10}
fmt.Println(p)
}
运行输出:
Tom / 10
看过上面代码,你应该已经注意到Go语言并没有使用implements
显示的指定该结构体实现了某某接口,它完全是隐式实现的,只要结构体上定义的方法实现能够对应到某一接口上,它就被认为是实现了某一接口。
这个特性只能说是自作聪明,设计的比较傻了,严重降低了代码可读性。要想在阅读别人代码时,知道一个结构体实现了哪个接口,不依赖IDE就很困难了。