go学习记录——第十二天

使用工厂方法创建结构体实例

结构体工厂

Golang不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在Go中实现“构造子工厂”方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以new...New...开头。假设定义了如下的File结构体实例。

1
2
3
4
type File struct {
fd int // 文件描述符
name string // 文件名
}

下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:

1
2
3
4
5
6
7
func NewFile(fd int, name, string) *File {
if fd < 0 {
return nil
}

return &File{fd, name}
}

然后用这个进行调用

1
f := NewFile(10, "./test.txt")

在Go中常常像这样在工厂方法里使用初始化来更加简单的实现函数的构造。

如果 File 是一个结构体类型,那么表达式 new(File)&File{} 是等价的。

这可以和大多数面向编程语言中笨拙的初始化方法做个比较(这么说真的好吗,笑死)File f = new File(...)

我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样(不应该是XX吗,为啥是OO,不懂)

如果想知道结构体类型 T 的一个实例占用了多少内存,可以使用 size := unsafe.Sizeof(T{})

如何强制使用工厂方法

通过应用可以性规则(参考4.2.1节9.5节)就可以禁止使用 new() 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样

1
2
3
4
5
6
7
8
type matrix struct {
...
}

func NewMatrix(params) *matrix {
m := new(matrix)
return m
}

在其他包里使用工厂方法:

1
2
3
4
5
6
7
package main
import "matrix"

...

wrong := new(matrix.matrix) // 编译失败(matrix是私有化的)
right := matrix.NewMatrix(...) // 实例化matrix的唯一方法

map 和 struct vs new() 和 make()

new()make() 这两个内置函数在 7.2.4节 通过切片的例子已经说明过一次(有一说一我真的记不得了,哭死)

现在为止我们已经见到了可以使用 make() 的三种类型中的其中两个:

1
slices / maps / channels (还在后面,第十四章)

在开始之前先回顾下吧(主要是我是真记不清了,时间隔太久了)

  1. 切片(Slice)

    语法:make([]T, length, capacity)

    T -> 元素类型

    length -> 切片当前长度

    capacity -> 切片的容量(可选参数,默认为length)

    1
    2
    slice := make([]int, 5)        // 创建一个长度为5的int切片,初始值为0
    slice := make([]string, 3, 10) // 创建一个长度为3、容量为10的string切片
  2. 映射(Map)

    语法: make(map[T1]T2, initialCapacity)

    T1 -> 键的类型

    T2 _> 是值的类型

    initialCapacity -> 预分配的存储空间大小(可选参数)

    1
    2
    m := make(map[string]int)          // 创建一个空的string到int的映射
    m := make(map[int]string, 10) // 创建一个预分配了10个存储空间的int到string的映射

    示例new_make.go(这个不能编译)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main

    type Foo map[string]string
    type Bar struct {
    thingOne string
    thingTwo int
    }

    func main() {
    // OK
    y := new(Bar)
    (*y).thingOne = "hello"

    // NOT OK
    z := make(Bar) // make(Bar) is wrong, should be make(Foo) or new(Bar)
    (*z).thingOne = "hello"
    (*z).thingTwo = 42

    // OK
    x := make(Foo)
    x["x"] = "goodbye"
    x["y"] = "world"

    // NOT OK
    u := new(Foo) // new(Foo) is wrong, should be make(Foo) or new(Bar)
    (*u)["x"] = "goodbye"
    (*u)["y"] = "world"
    }

分四段进行说明吧(书上写的太简略,我都怕我日后自己看给整晕)

​ 首先简单总结下 newmake 的区别

new 用于分配内存并返回只想类型的指针。对于结构体来说,调用 new 创建一个结构体类型的指针。

make 用于初始化切片(slices)、映射(maps)、通道(channels)并返回这个初始化后的引用,用于允许后续的操作。

​ 然后开始说四个部分

​ 在y这部分代码中,我们 new(Bar) 正确地创建了一个 Bar 类型的指针,并使用指针访问结构体字段。

​ 在z这部分代码中,我们使用 makr(Bar) 是错误的,因为 Bar 属于结构体,而不是 make() 所能使用的三个类型的其中之一

​ 在x这部分代码中,正确是因为 Foo 属于映射类型,是能够使用 make()

​ 在u这部分代码中,我们意图用 new 一个 Foo 类型的指针(实际上的返回结果是一个指向 nil 的指针),但这时返回的东西时不能够对其进行操作的,因为就是个指针,而我们所创建的是一个键值对,要对其赋值就需要通过 make() 去创建映射,然后才能对键值对进行操作。

使用自定义包中的结构体

下面的例子中,main.go 是使用了一个结构体,其来源于 struct_pack 下的包 structPack

示例 structPack.go

1
2
3
4
5
6
package structPack

type ExpStruct struct {
Mi1 int
MF1 float32
}

示例 main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
structPack "myproject/chapter10/struct_pack"
)

func main() {
struct1 := new(structPack.ExpStruct)
struct1.Mi1 = 19
struct1.MF1 = 3.

fmt.Printf("Mi1: %d\n", struct1.Mi1)
fmt.Printf("MF1: %f\n", struct1.MF1)
}

输出结果

1
2
Mi1: 19
MF1: 3.000000

带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。Tag的内容不可以在一般的编程中使用,只有包 reflect 能够获取它(详情见第11.10节 将详细讲解其功能,例如运行时自省类型、属性和方法)。

示例 struct_tag.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"reflect"
)

type TagType struct {
field1 bool "An important answer"
field2 string "The name of the thing"
field3 int "How much there are"
}

func main() {
tt := TagType{true, "Barak Obama", 1}
for i := 0; i < 3; i++ {
refTag(tt, i)
}
}
func refTag(tt TagType, ix int) {
ttType := reflect.TypeOf(tt)
ixField := ttType.Field(ix)
fmt.Printf("%v\n", ixField.Tag)
}

输出结果

1
2
3
An important answer
The name of the thing
How much there are

匿名字段和内嵌结构体

定义

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体

可以粗略地将这个和面向对象语言中的继承概念相比较(不懂,不会,比较不了一点),随后将会看到它被用来模拟类似继承的行为。Golang 中的继承是通过内嵌或组合来实现的,所以可以说,,在 Golang 中,相比较于继承,组合更受青睐。(好绕,懂不了一点,开玩笑的)

示例 structs_anonymous_fields.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"

type innerS struct {
in1 int
in2 int
}

type outerS struct {
b int
c float32
int // anonymous field
innerS // anonymous field
}

func main() {
outer := new(outerS)
outer.b = 6
outer.c = 7.5
outer.int = 60
outer.in1 = 10
outer.in2 = 20

fmt.Printf("outer.b = %d\n", outer.b)
fmt.Printf("outer.c = %f\n", outer.c)
fmt.Printf("outer.int = %d\n", outer.int)
fmt.Printf("outer.in1 = %d\n", outer.in1)
fmt.Printf("outer.in2 = %d\n", outer.in2)

// 使用结构体字面量
outer2 := outerS{6, 7.5, 60, innerS{10, 20}}
fmt.Println("outer2:", outer2)
}

输出结果

1
2
3
4
5
6
outer.b = 6
outer.c = 7.500000
outer.int = 60
outer.in1 = 10
outer.in2 = 20
outer2: {6 7.5 60 {10 20}}

通过类型 outer.int 的名字来获取存储在匿名字段中的数据,也就可以得到一个结论:在一个结构体中对于每一种数据只有一个匿名字段。

内嵌结构体

同样地,结构体也是一种数据类型,所以也可以作为一个匿名字段来使用,就如上面的例子那样。外层结构通过 outer.in1 直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或内嵌进外层结构体。这个简单的“继承“机制提供了一种方式,使得可以从另一个或一些类型继承部分或全部实现。(虽然但是,说得好复杂啊,不就是一个变量可以嵌入另一个变量使用,内部的东西不会改变吗)

在举个例子

示例 embedd_struct.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type A struct {
ax, ay int
}

type B struct {
A
bx, by float32
}

func main() {
b := B{A{1, 2}, 3.0, 5.0}
c := B{A{4, 6}, 7.7, 8.5}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(b.A)
fmt.Println(c.ax, c.ay, c.bx, c.by)
fmt.Println(c.A)
}

输出结果

1
2
3
4
1 2 3 5
{1 2}
4 6 7.7 8.5
{4 6}

这里例子中为了方便起见我设置了两个赋值,如果赋值是小数输出的就是float类型,如果是 .0 结尾会直接转为int,方便对比区别就设置了两个。

命名冲突

当两个字段拥有相同的名字(可能是继承来的名字,莫名想到了某些狗血番茄小说)时怎么办呢?

1. 外层名字会覆盖内层名字(但是两者的内存空间都被保留,我理解的就是内存不变,只是指向这两个的指针被改了名字,但指向还是具体的存储地址),这提供了一种重载字段或方法的方式;
1. 如果相同的名字出现在同一级别两次,如果这个名字被程序使用了就会报错(不使用就无所谓)。没有办法来解决这种问题,只能程序员自己改

举个例子

1
2
3
4
5
type A struct {a int}
type B struct {a, b int}

type C struct {A; B}
var c C

使用 c.a 是错误的,到底是 c.A.a 还是 c.B.a 。编译器是分不清的,会报错**ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a**

1
2
type D struct {B; b float32}
var d D

使用 d.b是没问题的,但是它是 float32 而不是 Bb 。如果想要内层的 b 可以通过 d.B.b得到

我捋一下,如果我们像第一个例子那样直接访问重名的变量,会导致编译器无法分清在哪个具体的结构体里的变量。而第二个例子里我们虽然D结构体里也有和B里一样的重名变量,但是我们直接用 d.b 是没办法取到 B.d 的,因为我们明确说明是需要取 D 里面的而不是B里面的,我们要取 B.b 则必须d直接取 B.b (好绕啊,反正意思就是那么个意思)


go学习记录——第十二天
https://www.lx02918.ltd/2024/12/01/go-study-twelfth-day/
作者
Seth
发布于
2024年12月1日
许可协议