go 支持使用 type 关键词来定义自己的类型,比如我们来定义一个 Enum 类型,go 默认没有提供 enum 类型,我们可以通过 type 自己定义一个枚举类型(在业务代码中经常用到枚举):
package main
import "fmt"
// 自定义一个 Enum类型
type Enum int
const (
// 这里如果是自增用 iota 更好
Init Enum = 0
Success Enum = 1
Fail Enum = 2
)
func main() {
fmt.Println(Init) // 0
}
上面我们自己定义了一个 Enum 类型,但是它的使用有些局限,比如你可以试试如下代码:
func main() {
status := 0
fmt.Println(Init == status) // main.go|18 col 19| invalid operation: Init == status (mismatched types Enum and int)
}
你会发现虽然 Enum 是使用 int 定义的,但是你是无法直接进行比较的,go 认为它们是不同的类型。怎么办呢? 你可以使用 int 来进行类型转换,比如使用 fmt.Println(int(Init) == status),这里我们使用另一个种方式, 就是给自定义类型增加方法(method)。
go 允许我们给自定义类型定义方法,所谓的方法其实就是有接收者(receiver)的函数,相较于普通函数,只不过多了一个接收者,你可以理解为方法就是有接收者的函数,它的格式如下:
func (r Receiver) functionName(optionalParameters) optionalReturnType {
body
}
比如我们要给 Enum 定一个方法叫做 Int(),它返回 Enum 对应的 int 值,可以这么写:
func (e Enum) Int() int {
return int(e)
}
这样一来就可以直接使用
func main() {
status := 0
fmt.Println(Init.Int() == status) // 调用 Init 的 Int 方法返回 int
}
一般业务代码里边我们还会给所有状态定义对应的中文或者英文字符串,完整的代码和使用示例如下:
package main
import "fmt"
// 自定义一个 Enum类型
type Enum int
const (
// 这里如果是自增用 iota 更好
Init Enum = 0
Success Enum = 1
Fail Enum = 2
// 枚举对应的中文
InitName = "初始化"
SuccessName = "成功"
FailName = "失败"
)
func (e Enum) Int() int {
return int(e)
}
func (e Enum) String() string {
return []string{
InitName,
SuccessName,
FailName,
}[e]
}
func main() { // 测试一下我们自己定义的 Enum
status := 0
fmt.Println(Init.Int() == status)
status2 := Fail
fmt.Println(status2.String())
}
go中是没有class关键字的,提供了 struct 用来定义自己的类型,struct 里可以放入需要的数据成员,并且可以给自定义 struct 增加方法。
我们开始使用 go 的 struct 来看一下 go 里边是如何实现类似其他编程语言的面向对象编程的。在 go 语言里,我们使用 struct 来定义一个新的类型,这和使用 class 非常像,比如这里我们定义一个简单的动物结构(类),包含 Name 和 Age 成员:
package main
import "fmt"
type Animal struct {
Name string
Age int
}
func main() {
a := Animal{Name: "dog", Age: 3}
fmt.Println(a, a.Name, a.Age)
}
ok,然后还可以给 struct 定义方法,比如动物都需要睡觉,所以我们给它添加一个方法叫做 Sleep:
package main
import "fmt"
type Animal struct {
Name string
Age int
}
func (a Animal) Sleep() {
fmt.Printf("%s is sleeping", a.Name)
}
func main() {
a := Animal{Name: "dog", Age: 3}
fmt.Println(a, a.Name, a.Age)
a.Sleep()
}
这样定义既有数据又有了方法,是不是和类比较像了,这就是在 go 中使用 OOP 的方式。当然了 OOP 还远不止这些,比如传统的 面向对象编程还有访问控制,构造函数,继承,多态等概念,我们会看一下它们是如何在 go 里边实现的,go 的方式和 Python/Java 等实现还是有挺大区别的。
Go 也有类似C++的访问控制,不过是通过数据和方法的命名首字母大小写决定的。在 Go 的包(package)中,只有首字母大写的才能被其他包导入使用,小写开头的则不行(在同一个包中,大写小写都可以被访问,被导入后,只能访问大写开头的数据成员和方法 )。所以一般结构体的私有数据成员和方法,我们使用小写开头,而公有的数据成员和方法,我们使用大写开头就好了。
我们现在给 Animal 加上一个私有的数据成员叫做 petName 表示动物的小名,同时新增一个方法叫做 SetPetName 用来设置它:
package main
import "fmt"
type Animal struct {
Name string
Age int
petName string
}
func (a Animal) Sleep() {
fmt.Printf("%s is sleeping, my petName is: %s\n", a.Name, a.petName)
}
func (a Animal) SetPetName(petName string) {
a.petName = petName
}
func main() {
a := Animal{Name: "dog", Age: 3, petName: "pet"}
fmt.Println(a, a.Name, a.Age)
a.Sleep()
a.SetPetName("little dog")
a.Sleep(
fmt.Println(a.petName) // 可以访问,因为在同一个包内。 但是没有修改成功!
fmt.Println("---")
}
为何没有修改成功?因为它属于值传递
函数的参数传递都是通过值进行传递的,也就是会复制参数的值,如果我们想要修改传入的值,就需要传递一个指针, 这就是为什么上边的 SetPetName 方法没有起作用的原因。如果我们想要修改一个结构体,就需要传入一个结构体指针作为接收者: (注意这里依然是值拷贝,不过拷贝的是指针的值而不是整个结构体的值,通过指针就可以修改对应的结构体)
package main
import "fmt"
type Animal struct {
Name string
Age int
petName string
}
func (a Animal) Sleep() {
fmt.Printf("%s is sleeping", a.Name)
}
func (a *Animal) SetPetName(petName string) {
a.petName = petName // NOTE: 这里的 a 是一个指针
// NOTE: 以下这种方式也是可以的,go 如果碰到指针会自动帮我们处理,所以使用起来更方便
// (*a).petName = petName
}
func main() {
aPtr := &Animal{Name: "dog", Age: 3}
aPtr.SetPetName("little dog")
fmt.Println(aPtr.petName) // 是不是可以设置成功了
}
go 里并没有像其他语言那样提供构造函数的方式来创建一个对象。 我们知道 go 里边创建一个空结构体的时候,不同类型的成员被赋值成其类型的『零值』,比如对于 Animal 里的 Name(string)是空字符串, Age(int) 是 0。
那如果我们想创建一个 Animal 的时候根据传入的参数来初始化呢?go 里边虽然没有直接提供构造函数,但是一般我们是通过定义一些 NewXXX 开头的函数来实现构造函数的功能的,比如我们可以定义一个 NewAnimal 函数:
package main
import "fmt"
type Animal struct {
Name string
Age int
petName string
}
func (a Animal) Sleep() {
fmt.Printf("%s is sleeping\n", a.Name)
}
func NewAnimal(name string, age int) *Animal {
a := Animal{
Name: name,
Age: age,
}
return &a
}
func main() {
a := NewAnimal("cat", 3)
fmt.Println(a)
}
面向对象 OOP 中还有一个重要的概念就是继承,通过继承实现了 is-a 的类关系,可以很好地进行代码复用。但是 go 可能又要让你失望了,go 并不直接支持 struct 之间的继承。
那如果我们想实现类似继承的功能该怎么办呢?其实 go 也有类似的解决方案,不过 go 使用的不是继承而是组合,go 作者推崇的 思想是『组合优于继承』。go 提供了结构体的嵌入(embedding)用来实现代码复用,比如如果我们想定义一个 Dog 结构体,Dog 也是一个 Animal,我们想复用 Animal 里的成员,可以在 Dog struct 里嵌入一个 Animal:
type Dog struct {
Animal // embedding
Color string
}
func main() {
d := Dog{}
d.Name = "dog"
d.Sleep()
}
你会发现在 Dog 里嵌入了 Animal 以后,我们就可以使用 Animal 的成员和方法了,从而实现了代码复用,是不是实现起来很简单。 我们还可以重写 Dog 自己的 Sleep 方法,来覆盖掉 Animal 的 Sleep 方法,给 Dog 增加一个方法:
type Dog struct {
Animal // embedding
Color string
}
func (d Dog) Sleep() {
fmt.Println("Dog method Sleep")
}
func main() {
d := Dog{}
d.Name = "dog"
d.Sleep() // 输出的是 Dog 的 Sleep 方法而不是 Animal 的
}
类似的,如果嵌入的 struct 里的成员名字和当前 struct 同名冲突了,go 会优先使用当前 struct 的成员。
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作,在go中,使用接口(inferface)实现多态。
go 的接口是一种抽象的自定义类型,没法直接实例化,它声明了一个或者多个方法的签名。如果一个 struct 实现了一个接口定义的所有方法,我们就说这个 struct 实现了这个接口。注意这里的『实现』是隐式的,你不用显示声明某个 struct 实现了哪个接口。
我们来看一个简单的例子,上一节学习 struct 时候,我们定义了一个 Animal,它有一个 Sleep 方法。这里我们定义一个叫做 Sleeper 的接口(go 喜欢用 er 给一个接口作为后缀,比如Reader/Writer):
// Sleeper 接口声明
type Sleeper interface {
Sleep() // 声明一个 Sleep() 方法
}
然后定义两个 struct,一个猫(Cat)和一个狗(Dog),并且它们都实了 Sleep 方法,也就是说隐式实现了 Sleeper 接口。
type Dog struct {
Name string
}
func (d Dog) Sleep() {
fmt.Printf("Dog %s is sleeping\n", d.Name)
}
type Cat struct {
Name string
}
func (c Cat) Sleep() {
fmt.Printf("Cat %s is sleeping\n", c.Name)
}
好了,然后我们编写一个函数,不过为了支持多态,函数的参数是一个接口类型而不是具体的 struct 类型。
func AnimalSleep(s Sleeper) {
s.Sleep()
}
完整代码如下:
package main
import (
"fmt"
)
// Sleeper 接口声明
type Sleeper interface {
Sleep() // 声明一个 Sleep() 方法
}
type Dog struct {
Name string
}
func (d Dog) Sleep() {
fmt.Printf("Dog %s is sleeping\n", d.Name)
}
type Cat struct {
Name string
}
func (c Cat) Sleep() {
fmt.Printf("Cat %s is sleeping\n", c.Name)
}
func AnimalSleep(s Sleeper) { // 注意参数是一个 interface
s.Sleep()
}
func main() {
var s Sleeper
dog := Dog{Name: "xiaobai"}
cat := Cat{Name: "hellokitty"}
s = dog
AnimalSleep(s) // 使用 dog 的 Sleep()
s = cat
AnimalSleep(s) // 使用 cat 的 Sleep()
// 创建一个 Sleeper 切片
sleepList := []Sleeper{Dog{Name: "xiaobai"}, Cat{Name: "kitty"}}
for _, s := range sleepList {
s.Sleep()
}
}
在上述代码中,可以用
Sleeper
指代两个不同的结构体,然后调用Sleeper
作为接收器的函数时,又能够调用各自结构的不同方法,这样就实现了多态。
个人理解:C++中的多态,(1)函数重载,根据参数的不同,调用同名函数时,会有不同的结果,这一点Go中是不支持的。 (2)虚函数,利用继承的关系,重写虚函数来实现多态,也就是利用基类指针,指向不同的子类时调用相同函数,会有不同的结果。在Go中能够实现类似的效果,但是Go中有点大道至简的感觉,为何 ? Go中利用接口,能够在不同结构struct
中实现多态,且不需要改写原有的代码结构;而C++中,需要重写继承关系,当关系较多时,较为繁琐。
我们知道 go 的 struct 可以通过嵌入实现代码复用,go 的接口也支持嵌入, 来看一个 go 标准库的例子。go 标准库里边定义了 Reader 和 Writer 接口如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
只要一个结构体实现了 Read 或者 Write 方法,它就分别实现了 Reader 和 Writer 接口。go 还支持接口嵌套,比如我们可以嵌套这俩 接口声明一个新的接口 ReadWriter:
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
我们也来试一下,刚才声明了 Sleeper 接口,再来声明一个叫做 Eater 的接口。有了睡和吃,我们再组合一下搞一个叫做 LazyAnimal 的接口(只知道吃和睡能不懒么?):
package main
import (
"fmt"
)
// Sleeper 接口声明
type Sleeper interface {
Sleep() // 声明一个 Sleep() 方法
}
type Eater interface {
Eat(foodName string) // 声明一个Eat 方法
}
type LazyAnimal interface {
Sleeper
Eater
}
type Dog struct {
Name string
}
func (d Dog) Sleep() {
fmt.Printf("Dog %s is sleeping\n", d.Name)
}
func (d Dog) Eat(foodName string) {
fmt.Printf("Dog %s is eating %s\n", d.Name, foodName)
}
type Cat struct {
Name string
}
func (c Cat) Sleep() {
fmt.Printf("Cat %s is sleeping\n", c.Name)
}
func (c Cat) Eat(foodName string) {
fmt.Printf("Cat %s is eating %s\n", c.Name, foodName)
}
func main() {
sleepList := []LazyAnimal{Dog{Name: "xiaobai"}, Cat{Name: "kitty"}}
foodName := "food"
for _, s := range sleepList {
s.Sleep()
s.Eat(foodName)
}
}
上文我们看到在使用接口的地方,我们可以传入一个具体的实现了接口的 struct 类型,但是我们如何获取传入的到底是哪种 struct 类型呢? go 给我们提供了一种方式叫做类型断言来获取具体的类型,它的语法比较简单,格式如下:
instance, ok := interfaceVal.(RealType) // 如果 ok 为 true 的话,接口值就转成了我们需要的类型
我们继续再上边的代码里加上类型断言的演示,注意类型断言那几行代码,再 for 循环里边我们使用类型断言获取了接口值的真正类型。
func main() {
sleepList := []LazyAnimal{Dog{Name: "xiaobai"}, Cat{Name: "kitty"}}
foodName := "food"
for _, s := range sleepList {
s.Sleep()
s.Eat(foodName)
// 类型断言 type assert
if dog, ok := s.(Dog); ok {
fmt.Printf("I am a Dog, my name is %s", dog.Name)
}
if cat, ok := s.(Cat); ok {
fmt.Printf("I am a Cat, my name is %s", cat.Name)
}
}
}
上文提到,如果一个 struct 实现了一个接口声明所有方法,我们就说这个 struct (隐式)实现了这个接口,那如果是一个没有声明 任何方法的空接口(empty interface)呢?按照这个定义岂不是所有类型都实现了空接口么?
你猜对了,所有类型都实现了空接口(interface{}),所以可以用空接口+类型断言转成任何我们需要的类型。来看下这个例子, 我们创建了一个空接口数组,它的元素可以是任何类型:
package main
import (
"fmt"
)
type Dog struct {
Name string
}
func (d Dog) Sleep() {
fmt.Printf("Dog %s is sleeping\n", d.Name)
}
type Cat struct {
Name string
}
func (c Cat) Sleep() {
fmt.Printf("Cat %s is sleeping\n", c.Name)
}
func main() {
animalList := []interface{}{Dog{Name: "xiaobai"}, Cat{Name: "kitty"}}
for _, s := range animalList {
if dog, ok := s.(Dog); ok {
fmt.Printf("I am a Dog, my name is %s\n", dog.Name)
}
if cat, ok := s.(Cat); ok {
fmt.Printf("I am a Cat, my name is %s\n", cat.Name)
}
}
}
那我们如何实现泛型呢?空接口其实给了我们思路。既然它能转成所有类型,那我们以空接口作为参数不就好了嘛,这个想法是对的。 如果你有留意的话,到现在我们的代码示例里边使用最多的是啥,其实是这句话 fmt.Println(),不知道你之前有没有发现这个函数 居然可以传递任意类型进去,用的是什么黑魔法呢?
既然我们知道了空接口,可以自己实现一个简单的可以打印多种类型的 MyPrint 函数
func MyPrint(i interface{}) {
switch o := i.(type) {
case int:
fmt.Printf("%d\n", o)
case float64:
fmt.Printf("%f\n", o)
case string:
fmt.Printf("%s\n", o)
default:
fmt.Printf("%+v\n", o)
}
}
func main() {
MyPrint(1)
MyPrint(4.2)
MyPrint("hello")
MyPrint(map[string]string{"hello": "go"})
}
实际上如果你用开发工具跳转包 fmt 对应 fmt.Println 的函数实现,可以看到它也是以空接口作为参数的:
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
空接口在实现泛型的时候很有用,不过一般情况下如果不是必要,我们还是单独实现对应类型的函数就好,代码可读性也更高。
参考:Let’s Go! Golang 短视频教程