Go 面向对象编程 1 | 青训营笔记

56 阅读10分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天。

  • Go 的面向对象只支持封装,不支持继承和多态

  • Go 没有类,是基于结构体来实现面向对象的

  • 没有构造函数、析构函数、this 指针

  • Go 通过结构体、方法、包来实现封装

1. 结构体(封装)

结构体的定义:

type 结构体类型名 struct {
    属性1: 属性1的类型
    属性2: 属性2的类型
    属性3: 属性3的类型
    // ...
}

定义一个表示二叉树数据结构的结构体:

type TreeNode struct {
    Value       int
    Left, Right *TreeNode
}
  • 结构体的每个字段都可以加上一个 tag,用于序列化

1.1. 创建结构体

// 方法一:按属性赋值
// 返回的是结构体本身
// 需要通过 & 操作符才能得到该结构体的指针
结构体类型名 {
    属性1: 值1,
    属性2: 值2,
    属性3: 值3,
    // ...
}

// 方法二:new
// 创建所有属性都为零值的结构体,并返回该结构体的地址
new(结构体类型名)
func new(Type) *Type

// 方法三:顺序赋值
结构体类型名 {
    属性1的值,
    属性2的值,
    属性3的值3,
    // ...
}
func main() {
    var root TreeNode
    root = TreeNode{}

    root.Value = 1
    root.Left = &TreeNode{}
    root.Right = &TreeNode{Value: 3}

    fmt.Println(root)       // {1 0xc000008078 0xc000008090}
    fmt.Println(root.Left)  // &{0 <nil> <nil>}
    fmt.Println(root.Right) // &{3 <nil> <nil>}

    var root2 = TreeNode{
        Value: 10,
        Left:  nil,
        Right: new(TreeNode),
    }
    fmt.Println(root2)       // {10 <nil> 0xc0000080f0}
    fmt.Println(root2.Left)  // <nil>
    fmt.Println(root2.Right) // &{0 <nil> <nil>}

    root2.Right.Left = &TreeNode{}
    fmt.Println(root2.Right)      // &{0 0xc000008138 <nil>}
    fmt.Println(root2.Right.Left) // &{0 <nil> <nil>}


    root3 := TreeNode{10, nil, nil}
    fmt.Println(root3) // {10 <nil> <nil>}
}
  • 不论是通过结构体本身还是结构体指针来访问属性,都是用 . 运算符
  • 结构体是值类型
var n1 = TreeNode{Value: 1}

var n2 = n1
n2.Value = 2

fmt.Println(n1, n2) // {1 <nil> <nil>} {2 <nil> <nil>}

1.2. 工厂函数

func createTreeNode(value int) *TreeNode {
    // 返回了一个局部变量的地址竟然是可用的!!!
    return &TreeNode{
        Value: value,
    }
}
func main() {
    root := createTreeNode(10)
    fmt.Println(root)  // &{10 <nil> <nil>}

    root.Left = createTreeNode(11)
    root.Right = createTreeNode(12)
    root.Left.Left = createTreeNode(13)

    fmt.Println(root)           // &{10 0xc0000081b0 0xc0000081c8}
    fmt.Println(root.Left)      // &{11 0xc0000081e0 <nil>}
    fmt.Println(root.Right)     // &{12 <nil> <nil>}
    fmt.Println(root.Left.Left) // &{13 <nil> <nil>}
}
  • 在 Go 中如果一个结构体会被函数返回出去,则该结构体被创建在堆上;如果不被返回出去的创建在栈上。
  • Go 编译器会尽可能将变量分配在栈上. 以下两种情况,Go 编译器会将变量分配在堆上:1. 如果一个变量被取地址,并且被逃逸分析为逃逸到堆;2. 该变量很大。

1.3. 方法

方法的定义

func (方法接收者) 方法名 (形参列表) (返回值列表) {
    方法体
}
  • 方法接收者可以是结构体,也可以是其他自定义类型
  • 方法接收者传递和参数的传递是一样的,按值传递。
    因此下面无论是方法还是函数都是改不了 Value 的值的。
    需要使用 TreeNode 的指针
type TreeNode struct {
    Value       int
    Left, Right *TreeNode
}

func functionSetValue(node TreeNode, value int) {
    node.Value = value
    fmt.Println(node) // {11 <nil> <nil>}
}

func (node TreeNode) methodSetValue(value int) {
    node.Value = value
    fmt.Println(node) // {12 <nil> <nil>}
}

func main() {
    node := TreeNode{Value: 1}

    functionSetValue(node, 11)
    fmt.Println(node) // {1 <nil> <nil>}

    node.methodSetValue(12)
    fmt.Println(node) // {1 <nil> <nil>}
}

改为指针类型

func functionSetValue(node *TreeNode, value int) {
    node.Value = value
    fmt.Println(node) // &{11 <nil> <nil>}
}

func (node *TreeNode) methodSetValue(value int) {
    node.Value = value
    fmt.Println(node) // &{12 <nil> <nil>}
}

func main() {
    node := TreeNode{Value: 1}

    // functionSetValue(node, 11) // 报错: cannot use node (variable of type TreeNode) as type *TreeNode in argument to functionSetValue
    // 必须用 & 操作符得到指针类型
    functionSetValue(&node, 11)
    fmt.Println(node) // {11 <nil> <nil>}

    // 无论是结构体本身还是结构体指针, 都可以使用 . 操作符
    node.methodSetValue(12)
    fmt.Println(node) // {12 <nil> <nil>}
}

nil 指针也可以调用方法,不会空指针异常。但在方法内取属性会出现异常

func (node *TreeNode) nullPointer() {
    fmt.Println(node == nil)
    fmt.Println(node)
    fmt.Printf("%p\n", node)
}

func main() {
    var n *TreeNode
    n.nullPointer()
    // true             
    // <nil>            
    // 0x0 
}

1.4. 内存对齐

  • 结构体的内存分配会按照一个系数(unsafe.Alignof)进行字节对齐。

  • 非内存对齐会影响内存操作的原子性和效率。

  • 基本数据类型的对齐:某个类型的变量的地址必须被改类型的对齐系统所整除

fmt.Printf("bool size:%d align: %d\n",  unsafe.Sizeof(bool(true)), unsafe.Alignof(bool(true)))
fmt.Printf("byte size:%d align: %d\n",  unsafe.Sizeof(byte(0)),    unsafe.Alignof(byte(0)))
fmt.Printf("int8 size:%d align: %d\n",  unsafe.Sizeof(int8(0)),    unsafe.Alignof(int8(0)))
fmt.Printf("int16 size:%d align: %d\n", unsafe.Sizeof(int16(0)),   unsafe.Alignof(int16(0)))
fmt.Printf("int32 size:%d align: %d\n", unsafe.Sizeof(int32(0)),   unsafe.Alignof(int32(0)))
fmt.Printf("int64 size:%d align: %d\n", unsafe.Sizeof(int64(0)),   unsafe.Alignof(int64(0)))
fmt.Printf("string size:%d align: %d\n", unsafe.Sizeof(""), unsafe.Alignof(""))
// bool  size:1 align: 1
// byte  size:1 align: 1
// int8  size:1 align: 1
// int16 size:2 align: 2
// int32 size:4 align: 4
// int64 size:8 align: 8
// string size:16 align: 8
  • 结构体内存对齐:内部对齐 + 长度填充(结构体之间对齐)
    • 内部对齐:每个成员的偏移量是自身大小与其对齐系数中较小值的整数倍,严格按照成员顺序来存放
    • 长度填充:整个结构体的长度必须是最大成员长度与系统字长中较小值的整数倍
type A struct {
    a int32  // 4 字节
    b int32  // 4 字节
}

type B struct {
    a int16  // 2 字节
    b int32  // 4 字节
}

type C struct {
    a int16  // 2 字节
    b int32  // 4 字节
    c int32  // 4 字节
}

func main() {
    fmt.Println(unsafe.Sizeof(A{})) // 8
    fmt.Println(unsafe.Sizeof(B{})) // 8
    fmt.Println(unsafe.Sizeof(C{})) // 12
}
type D struct {
            // 自身大小 对齐系数
    a bool  // 1        1
    b int32 // 4        4
    c int16 // 2        2
    d int64 // 8        8
}

d := D{}
fmt.Println(unsafe.Sizeof(d))
fmt.Printf("%p\n", &d)
fmt.Printf("%p\n", &d.a)
fmt.Printf("%p\n", &d.b)
fmt.Printf("%p\n", &d.c)
fmt.Printf("%p\n", &d.d)
// 24
// 0xc000012240  0
// 0xc000012240  +0
// 0xc000012244  +4
// 0xc000012248  +8
// 0xc000012250  +10
type User struct {
    //           size align:
    A int32   // 4      4
    B []int32 // 24   8
    C string  // 16   8
    D bool    // 1    1
    E struct{}
}

fmt.Printf("string size:%d align: %d\n", unsafe.Sizeof(""), unsafe.Alignof(""))
// string size:16 align: 8
fmt.Printf("[]int32 size:%d align: %d\n", unsafe.Sizeof([]int32{}), unsafe.Alignof([]int32{}))
// []int32 size:24 align: 8

u := User{}
fmt.Printf("User size:%d align: %d\n", unsafe.Sizeof(u), unsafe.Alignof(u))
// User size:56 align: 8
fmt.Printf("%p\n", &u)
fmt.Printf("%p\n", &u.A)
fmt.Printf("%p\n", &u.B)
fmt.Printf("%p\n", &u.C)
fmt.Printf("%p\n", &u.D)
fmt.Printf("%p\n", &u.E)
// 0xc000024080 0
// 0xc000024080 +0
// 0xc000024088 +8
// 0xc0000240a0 +32
// 0xc0000240b0 +48
// 0xc0000240b1 +49

空结构体作为成员时,地址跟随前一成员的末尾;当空结构体在末尾且结构体刚好不需要长度填充时,为防止其地址溢出到结构体外部,需要对结构体再进行一个单位的长度填充。

type E1 struct {
    a int32 // 4
    s struct{}
    b int32 // 4
}

type E2 struct {
    a int32 // 4
    b int32 // 4
    s struct{}
}

func main() {
    e1 := E1{}
    fmt.Println(unsafe.Sizeof(e1))
    fmt.Printf("%p\n", &e1)
    fmt.Printf("%p\n", &e1.a)
    fmt.Printf("%p\n", &e1.s)
    fmt.Printf("%p\n", &e1.b)
    // 8
    // 0xc000018198
    // 0xc000018198
    // 0xc00001819c
    // 0xc00001819c

    e2 := E2{}
    fmt.Println(unsafe.Sizeof(e2))
    fmt.Printf("%p\n", &e2)
    fmt.Printf("%p\n", &e2.a)
    fmt.Printf("%p\n", &e2.b)
    fmt.Printf("%p\n", &e2.s)
    // 12
    // 0xc0000181a4
    // 0xc0000181a4
    // 0xc0000181a8
    // 0xc0000181ac
}

2. 包(封装)

包的作用

  1. 区分同名的代码结构
  2. 方便组织和管理项目代码
  3. 控制变量、函数、方法、结构体的访问范围

package

  • 每个目录一个包,也就是说一个目录下的 .go 源文件中 package 的包名必须一致。
  • 包名和目录名可以不一致,但一般来说包名和最后一级目录保持一致。
  • main 包下的 main 函数才是可执行的入口,其他包下的 main 函数不可作为入口执行。
  • 一个包内的变量、函数、方法、结构体不能用同名的;不同包之间可以有同名的。
    即,包作用域内不允许重复定义。
  • 结构的定义和结构的方法的定义必须在同一个目录的同一个包内,可以分文件的。
    不同目录但相同包名其实是算两个不同的包,只是包名相同。
  • 变量、函数、方法、结构体的访问权限有两种:以大写字母开头的包外可访问,以小写字母或者下划线开头开头的仅包内可见

import

  • import 包名或别名 包所对应的目录。当包名和最后一级目录一致时,可省略包名或别名,直接使用包名。
  • 引入了不同目录下的同名包时,需要取别名区分。
  • 使用 go mod 依赖管理时,当前 module 的包引入是需要以当前 module 名开头的,即 moduleName/XXX/YYY/ZZZ
  • 当不是以当前 module 名开头的 import XXX/YYY/ZZZ 的寻找路径为:
    1. GOROOT/src/XXX/YYY/ZZZ
    2. GOPATH/src/XXX/YYY/ZZZ
  • 其他包的变量、函数、结构体是使用是 包名.变量包名.函数(...)包名.结构体

下面是一个实例的目录结构:

awesomeProject
|-- go.mod
|-- package
    |-- main.go
    |-- tree1
    |   |-- TreeNode.go
    |-- tree2
        |-- TreeNode.go
    |-- tree3
        |-- TreeNode.go

go.mod

module awesomeProject

go 1.19

package/tree1/TreeNode.go

package tree1

type TreeNode struct {
    Value       int
    Left, Right *TreeNode
}

package/tree2/TreeNode.go

package tree2c

type TreeNode struct {
    Value       int
    Left, Right *TreeNode
}

package/tree3/TreeNode.go

package tree1

type TreeNode struct {
    Value       int
    Left, Right *TreeNode
}

package/main.go

package main

import (
    "awesomeProject/package/tree1"
    tree2c "awesomeProject/package/tree2" // 包名和目录不一致时, 需要取别名
    tree3 "awesomeProject/package/tree3"  // 不同目录下有相同包名时, 需要取别名
    "fmt"
)

func main() {
    n1 := tree1.TreeNode{Value: 1}
    fmt.Println(n1)

    n2 := tree2c.TreeNode{Value: 2}
    fmt.Println(n2)

    n3 := tree3.TreeNode{Value: 2}
    fmt.Println(n3)
}

3. 继承

3.1. 用类型的别名来实现继承

通过定义一个 slice 的别名来实现一个队列:

package main

import "fmt"

type Queue []int

func (q *Queue) Push(x int) {
    *q = append(*q, x)
}

func (q *Queue) Pop() int {
    x := (*q)[0]
    *q = (*q)[1:]
    return x
}

func (q *Queue) IsEmpty() bool {
    return len(*q) == 0
}

func main() {
    queue := Queue{}

    queue.Push(1)
    queue.Push(2)
    queue.Push(3)
    fmt.Println(queue)
    // [1 2 3]

    for !queue.IsEmpty() {
        popX := queue.Pop()
        fmt.Println(popX, queue)
    }
    // 1 [2 3]
    // 2 [3]
    // 3 []
}

可以发现每次支持 PushPop 后 queue 都会发生变化

queue := Queue{}
fmt.Printf("%p\n", queue)

for i := 0; i < 5; i++ {
    queue.Push(i)
    fmt.Printf("%p\n", queue)
}
for !queue.IsEmpty() {
    queue.Pop()
    fmt.Printf("%p\n", queue)
}
// 0x5ea5a0
// 0xc00001c138
// 0xc00001c170
// 0xc000014200
// 0xc000014200
// 0xc000010240
// 0xc000010248
// 0xc000010250
// 0xc000010258
// 0xc000010260
// 0xc000010268

3.2. 用组合实现继承(最常用)

现在我们有一个二叉树的基础结构体,其只实现了中序遍历的方法。我们需要拓展前序遍历的方法。

awesomeProject/extends/tree/node.go

package tree

import "fmt"

type Node struct {
    Value       int
    Left, Right *Node
}

func CreateTreeNode(value int) *Node {
    return &Node{
        Value: value,
    }
}

func (root *Node) Mid() {
    if root == nil {
        return
    }

    root.Left.Mid()
    root.Print()
    root.Right.Mid()
}

func (root *Node) Print() {
    fmt.Printf("%d ", root.Value)
}

用组合来拓展一个 MyNode 结构体,增加一个前序遍历的方法:

package main

import (
    "awesomeProject/extends/tree"
    "fmt"
)

type MyNode struct {
    node *tree.Node
}

func (root *MyNode) Pre() {
    if root == nil || root.node == nil {
        return
    }

    root.node.Print()

    leftMyNode := MyNode{root.node.Left}
    leftMyNode.Pre()

    rightMyNode := MyNode{root.node.Right}
    rightMyNode.Pre()
}

func main() {
    node := tree.Node{
        Value: 1,
        Left:  &tree.Node{Value: 2},
        Right: &tree.Node{Value: 3},
    }
    node.Mid() // 2 1 3

    fmt.Println()

    myNode := MyNode{
        &node,
    }
    myNode.Pre() // 1 2 3
}

3.3. 用内嵌匿名结构体来实现继承(组合方式的语法糖)

package main

import (
    "awesomeProject/extends/tree"
    "fmt"
)

type MyNode struct {
    *tree.Node  // 内嵌匿名结构体
}

func (root *MyNode) Pre() {
    // 相当于定义了一个名为 Node 的属性
    // 这里就不能简化为 root == nil 了, 必须判断内嵌的匿名 Node 是否为空
    if root == nil || root.Node == nil {
        return
    }

    // 不需要 root.Node 了, 语法糖简化
    root.Print()

    leftMyNode := MyNode{root.Left}
    leftMyNode.Pre()

    rightMyNode := MyNode{root.Right}
    rightMyNode.Pre()
}

func main() {
    node := tree.Node{
        Value: 1,
        Left:  &tree.Node{Value: 2},
        Right: &tree.Node{Value: 3},
    }
    node.Mid() // 2 1 3

    fmt.Println()

    myNode := MyNode{
        &node,
    }
    myNode.Pre() // 1 2 3
}

3.3.1. shadowed

MyNode 中定义了一个方法名和形参列表和 tree.Node 一样的方法时,tree.Node 的方法会被隐藏。
类似于 Java 的重写方法。

func (root *MyNode) Mid() {
    fmt.Println("tree.Node.Mid is shadowed")
}


func main() {
    myNode.Mid()      // tree.Node.Mid is shadowed
    myNode.Node.Mid() // 2 1 3
    // 但和重写不一样, 还是可以调用到 tree.Node 的方法的
}