我第一次准备转 Golang 的时候问同事说 Golang 是不是面向对象的?有没有 Class?
我同事说 “没有”,我第一反应是 “啊?!”
“但可以用 struct 做类似的东西”,我 “嗯????!”
那么 Golang 到底能不能面向对象呢?卖个关子,这一章先介绍一下什么是 struct 吧~
推荐代码结构
为了更好上手,可以参考下面的代码结构边看边动手哈~
hello-go
│
└─ tutorial
│ └─ ep07
│ │ └─ ...
│ └─ ep08
│ └─ demo_struct_basic.go
│ └─ ...
│
└─ main.go
└─ go.mod
// main.go
package main
import (
"kabuski/hello-go/tutorial/ep08"
)
func main(){
// 改成下面要测试的示例函数名
ep08.XXXXXXX()
}
代码示例
Demo 1: 数据结构 struct 的基础用法
// demo_struct_basic.go
package ep08
import (
"fmt"
)
type Block struct {
A int
b bool
}
type BlockCompose struct {
Blk Block
C int
}
type BlockInherit struct {
Block
C int
}
上面有一共定义了 4 个 struct,直译为 (数据) 结构。
第 1 个 Block 是一个最基础的结构,每一行声明一个成员变量,同样是先 {变量名} 后 {数据类型}。
第 2 个 BlockCompose 是一个 组合 (Composition) 的示例,即 Blk Block 作为 BlockCompose 的一个成员变量。如果改成 BlkPtr *Block 当然也是可以的,这个应该比较好理解。
第 3 个 BlockInherit 是一个 继承 (Inheritance) 的示例,和成员变量不同,在语法上只需要 struct 名即可 (如 Block)。
接下來先介绍一个通用的函数,可以用来显示任意一个变量的信息
// demo_struct_basic.go
// ...
func PrintInfo(v interface{}){
fmt.Printf("%%T: %T\n", v)
fmt.Printf("%%v: %v\n", v)
fmt.Printf("%%#v: %#v\n", v)
}
其中 interface{} 可以先理解为一个万用的数据类型,后面会再讨论。%T 和 %v 在之前的章节裡也提过了,用于显示变量的数据类型和值。这里再介绍一个新的 %#v,可以更好地显示 struct 的內容,具体看下面的示例就懂喇~
// demo_struct_basic.go
// ...
func DemoBasic(){
blk := Block{}
fmt.Println("- Basic - CASE1")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
// fmt.Printf("blk.b: %v\n", blk.b) // 会报错
fmt.Println()
blk = Block{
A: 3,
b: false,
}
fmt.Println("- Basic - CASE2")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
// fmt.Printf("blk.b: %v\n", blk.b) // 会报错
fmt.Println()
}
运行结果如下
- Basic - CASE1
blk
%T: ep08.Block
%v: {0 false}
%#v: ep08.Block{A:0, b:false}
blk.A: 0
- Basic - CASE2
blk
%T: ep08.Block
%v: {3 false}
%#v: ep08.Block{A:3, b:false}
blk.A: 3
Point 1 struct 最基础的实例化方法是 结构名{} (如 Block{})
Point 2 要初始化 struct 的成员变量就是 变量名: 初始值, (如 A: 3,),注意 结尾的逗号很重要!
Point 3 不要必须初始化所有成员变量
Point 4 只有首字母大写的成员 (如 Block.A) 可以在外部被调用,相当于 public,否则 (如 Block.b) 不可以被调用,相当于 private。
接下來看看 组合的 struct 的使用方法~
// demo_struct_basic.go
// ...
func DemoCompose(){
blk := BlockCompose{}
fmt.Println("- DemoCompose - CASE1")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.Blk.A: %d\n", blk.Blk.A)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Println()
blk = BlockCompose{
Blk: Block{
A: 3,
b: false,
},
C: 4,
}
fmt.Println("- DemoCompose - CASE2")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.Blk.A: %d\n", blk.Blk.A)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Println()
}
运行结果如下
- DemoCompose - CASE1
blk
%T: ep08.BlockCompose
%v: {{0 false} 0}
%#v: ep08.BlockCompose{Blk:ep08.Block{A:0, b:false}, C:0}
blk.Blk.A: 0
blk.C: 0
- DemoCompose - CASE2
blk
%T: ep08.BlockCompose
%v: {{3 false} 4}
%#v: ep08.BlockCompose{Blk:ep08.Block{A:3, b:false}, C:4}
blk.Blk.A: 3
blk.C: 4
其中展示了组合的 struct 的调用方法和初始化方法,可以看出来其实就是在基础用法上套一层,语法原理上是一样的。
最后看一下 继承的 struct 的使用方法~
// demo_struct_basic.go
// ...
func DemoInherit(){
blk := BlockInherit{}
fmt.Println("- DemoInherit - CASE1")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Println()
blk = BlockInherit{
Block: Block{
A: 3,
b: false,
},
C: 4,
}
fmt.Println("- DemoInherit - CASE2")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Println()
fmt.Println("blk.Block")
PrintInfo(blk.Block)
fmt.Printf("blk.Block.A: %d\n", blk.Block.A)
// fmt.Printf("blk.Block.C: %d\n", blk.Block.C)
fmt.Printf("(unavailable) blk.Block.C\n")
fmt.Println()
}
运行结果如下
- DemoInherit - CASE1
blk
%T: ep08.BlockInherit
%v: {{0 false} 0}
%#v: ep08.BlockInherit{Block:ep08.Block{A:0, b:false}, C:0}
blk.A: 0
blk.C: 0
- DemoInherit - CASE2
blk
%T: ep08.BlockInherit
%v: {{3 false} 4}
%#v: ep08.BlockInherit{Block:ep08.Block{A:3, b:false}, C:4}
blk.A: 3
blk.C: 4
blk.Block
%T: ep08.Block
%v: {3 false}
%#v: ep08.Block{A:3, b:false}
blk.Block.A: 3
(unavailable) blk.Block.C
可以看出來继承的 struct 可以直接调用自身的成员(如 blk.C),同时也可以直接调用继承的 struct 的成员(如 blk.A)。
有面向对象经验的同学应该就会有疑问,我怎么 把一个子类的对象作为父类的对象使用 呢?方法就是 {变量名}.{父类名} (如 blk.Block) ,当然作为父类之后能够调父类本身的成员 (如 blk.Block.A),但不能再调用子类新声明的成员 (如 blk.Block.C 就不存在)。
Demo 2: 继承一个指针类型?
在上一章讨论指针的时候说过,一个数据类型的指针也是一个数据类型,同理 struct 的指针也是一个数据类型。那如果 struct 继承一个指针类型会怎样?
// demo_struct_basic.go
// ...
type BlockInheritPtr struct {
*Block
C int
}
func DemoInheritPtr(){
blkPtr := BlockInheritPtr{}
fmt.Println("- DemoInheritPtr - CASE1")
fmt.Println("blkPtr")
PrintInfo(blkPtr)
// fmt.Printf("blkPtr.A: %d\n", blkPtr.A) // 会报错
fmt.Printf("(unavailable) blkPtr.A\n")
fmt.Printf("blkPtr.C: %d\n", blkPtr.C)
fmt.Println()
blk := Block{
A: 3,
b: false,
}
blkPtr = BlockInheritPtr{
Block: &blk,
C: 4,
}
fmt.Println("- DemoInheritPtr - CASE2")
fmt.Println("blkPtr")
PrintInfo(blkPtr)
fmt.Printf("blkPtr.A: %d\n", blkPtr.A)
fmt.Printf("blkPtr.C: %d\n", blkPtr.C)
fmt.Println()
fmt.Println("blkPtr.Block")
PrintInfo(blkPtr.Block)
fmt.Printf("blkPtr.Block.A: %d\n", blkPtr.Block.A)
// fmt.Printf("blkPtr.Block.C: %d\n", blkPtr.Block.C)
fmt.Printf("(unavailable) blkPtr.Block.C\n")
fmt.Println()
}
运行结果如下
- DemoInheritPtr - CASE1
blkPtr
%T: ep08.BlockInheritPtr
%v: {<nil> 0}
%#v: ep08.BlockInheritPtr{Block:(*ep08.Block)(nil), C:0}
(unavailable) blkPtr.A
blkPtr.C: 0
- DemoInheritPtr - CASE2
blkPtr
%T: ep08.BlockInheritPtr
%v: {0xc0000b6110 4}
%#v: ep08.BlockInheritPtr{Block:(*ep08.Block)(0xc0000b6110), C:4}
blkPtr.A: 3
blkPtr.C: 4
blkPtr.Block
%T: *ep08.Block
%v: &{3 false}
%#v: &ep08.Block{A:3, b:false}
blkPtr.Block.A: 3
(unavailable) blkPtr.Block.C
也是没问题的~ 可以看到当指针没有初始化的话,指针的值为 nil,此时是不能直接调用这个子类的成员的 (如 blk.A),更准确地说这个成员根本不存在。而指针指向一个实例之后,用法就和前面的 BlockInherit 一样了。
Demo3: 如果同时继承多个 struct 呢?
// demo_struct_basic.go
// ...
type BlockA struct {
Data int
A int
}
type BlockB struct {
Data int
B int
}
type BlockInheritTwo struct {
BlockA
BlockB
C int
}
func DemoInheritTwo(){
blk := BlockInheritTwo{}
fmt.Println("- BlockInheritTwo - CASE1")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
fmt.Printf("blk.B: %d\n", blk.B)
fmt.Printf("blk.C: %d\n", blk.C)
// fmt.Printf("blk.Data: %d\n", blk.Data)
fmt.Println("(unavailable) ambiguous selector blk.Data")
fmt.Println()
blk = BlockInheritTwo{
BlockA: BlockA{
Data: 1,
A: 10,
},
BlockB: BlockB{
Data: 2,
B: 20,
},
C: 4,
}
fmt.Println("- BlockInheritTwo - CASE2")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.A: %d\n", blk.A)
fmt.Printf("blk.B: %d\n", blk.B)
fmt.Printf("blk.C: %d\n", blk.C)
// fmt.Printf("blk.Data: %d\n", blk.Data)
fmt.Println("(unavailable) ambiguous selector blk.Data")
fmt.Println()
fmt.Println("blk.BlockA")
PrintInfo(blk.BlockA)
fmt.Printf("blk.BlockA.Data: %d\n", blk.BlockA.Data)
fmt.Println()
fmt.Println("blk.BlockB")
PrintInfo(blk.BlockB)
fmt.Printf("blk.BlockB.Data: %d\n", blk.BlockB.Data)
fmt.Println()
}
运行结果如下
- BlockInheritTwo - CASE1
blk
%T: ep08.BlockInheritTwo
%v: {{0 0} {0 0} 0}
%#v: ep08.BlockInheritTwo{BlockA:ep08.BlockA{Data:0, A:0}, BlockB:ep08.BlockB{Data:0, B:0}, C:0}
blk.A: 0
blk.B: 0
blk.C: 0
(unavailable) ambiguous selector blk.Data
- BlockInheritTwo - CASE2
blk
%T: ep08.BlockInheritTwo
%v: {{1 10} {2 20} 4}
%#v: ep08.BlockInheritTwo{BlockA:ep08.BlockA{Data:1, A:10}, BlockB:ep08.BlockB{Data:2, B:20}, C:4}
blk.A: 10
blk.B: 20
blk.C: 4
(unavailable) ambiguous selector blk.Data
blk.BlockA
%T: ep08.BlockA
%v: {1 10}
%#v: ep08.BlockA{Data:1, A:10}
blk.BlockA.Data: 1
blk.BlockB
%T: ep08.BlockB
%v: {2 20}
%#v: ep08.BlockB{Data:2, B:20}
blk.BlockB.Data: 2
没问题的!子 struct 可以同时使用各个父 struct 的成员(如 blk.A 和 blk.B )。不过如果两个父 struct 有相同名字的成员 ( 如 BlockA.Data 和 BlockB.Data ),那么这个成员就不能被子 struct 的实例直接调用 (如 blk.Data),程序在编译阶段就会报错 ambiguous selector 。但转換回对应的父 struct 再调用的话是没有问题的 (如 blk.BlockA.Data 和 blk.BlockB.Data )。
Demo4: 如果套娃呢?
// demo_struct_basic.go
// ...
type BlockC struct {
C int
D int
}
type BlockC2 struct {
BlockC
C2 int
D int
}
type BlockC3 struct {
BlockC2
C3 int
D int
}
func DemoInheritPlus(){
blk := BlockC3{}
fmt.Println("- DemoInheritPlus - CASE1")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.C3: %d\n", blk.C3)
fmt.Printf("blk.C2: %d\n", blk.C2)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Printf("blk.D: %d\n", blk.D)
fmt.Printf("blk.BlockC2.D: %d\n", blk.BlockC2.D)
fmt.Println()
blk = BlockC3{
BlockC2: BlockC2{
BlockC: BlockC{
C: 1,
D: 1,
},
C2: 2,
D: 2,
},
C3: 3,
D: 3,
}
fmt.Println("- DemoInheritPlus - CASE2")
fmt.Println("blk")
PrintInfo(blk)
fmt.Printf("blk.C3: %d\n", blk.C3)
fmt.Printf("blk.C2: %d\n", blk.C2)
fmt.Printf("blk.C: %d\n", blk.C)
fmt.Printf("blk.D: %d\n", blk.D)
fmt.Printf("blk.BlockC2.D: %d\n", blk.BlockC2.D)
fmt.Println()
}
运行结果如下
- DemoInheritPlus - CASE1
blk
%T: ep08.BlockC3
%v: {{{0 0} 0 0} 0 0}
%#v: ep08.BlockC3{BlockC2:ep08.BlockC2{BlockC:ep08.BlockC{C:0, D:0}, C2:0, D:0}, C3:0, D:0}
blk.C3: 0
blk.C2: 0
blk.C: 0
blk.D: 0
blk.BlockC2.D: 0
- DemoInheritPlus - CASE2
blk
%T: ep08.BlockC3
%v: {{{1 1} 2 2} 3 3}
%#v: ep08.BlockC3{BlockC2:ep08.BlockC2{BlockC:ep08.BlockC{C:1, D:1}, C2:2, D:2}, C3:3, D:3}
blk.C3: 3
blk.C2: 2
blk.C: 1
blk.D: 3
blk.BlockC2.D: 2
可以看出来孙子 struct(如 blk BlockC3)调用爷 struct 的成员 (如 BlockC.C) 也是没问题的。而对于同名的成员 (如 BlockC3.D、BlockC2.D、BlockC.D),则是取最年轻的那个(如 blk.D 对应BlockC3.D,blk.BlockC2.D 对应 BlockC2.D)。
结语
本章介绍了 Golang 中数据结构 struct 的百种声明方法 (误)。
其实在面向对象编程中,最常用的是组合,继承的使用场景相对较少,更别说同时继承多个 struct 和多层继承的场景。
这种探索一方面是出于好奇心,而另一方面是进一步了解 Golang 这套语言的设计思路。除了知道这个语言怎么用以外,我还会想这个语言为什么会这样设计。譬如在 Demo3 中,如果是你设计的话,对于同名的成员变量,如果取从上到下第一个继承的 struct 的成员会不会更方便一点? 或者说会不会在大部分场景下更方便一点?
没想到这么点东西写了这么长,本来想一下午连 struct 的函数声明方法也写的,结果工作原因再拖了两个礼拜,这一章再不收尾要长猫了。那么 struct 的函数声明就在下一章继续喇~
下一章见~