欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

结构(struct)与方法(method)

程序员文章站 2024-03-13 11:58:21
...

概述

Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。
组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录(Record),比如 Cobol,在 C 家族的编程语言中它也存在,并且名字也是 struct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。不过因为 Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。

结构体的定义

结构体定义的一般方式如下:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
结构体的字段可以是任何类型,甚至是结构体本身(参考第 10.5 节),也可以是函数或者接口(参考第 11 章)。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值

var s T
s.a = 5
s.b = 8

数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。

使用 new

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。

var t *T
t = new(T)

写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。

package main
import "fmt"

type struct1 struct {
    i1  int
    f1  float32
    str string
}

func main() {
    ms := new(struct1)
    ms.i1 = 10
    ms.f1 = 15.5
    ms.str= "Chris"

    fmt.Printf("The int is: %d\n", ms.i1)
    fmt.Printf("The float is: %f\n", ms.f1)
    fmt.Printf("The string is: %s\n", ms.str)
    fmt.Println(ms)
}

输出

The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

使用 fmt.Println 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value。
同样的,使用点号符可以获取结构体字段的值:structname.fieldname。

type myStruct struct { i int }
var v myStruct    // v是结构体类型变量
var p *myStruct   // p是指向一个结构体类型变量的指针
v.i
p.i

初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:

  ms := &struct1{10, 15.5, "Chris"}
    // 此时ms的类型是 *struct1

或者:

 var ms struct1
    ms = struct1{10, 15.5, "Chris"}

混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用 new
(),这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 new(Type) 和
&Type{} 是等价的。 时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:

type Interval struct {
    start int
    end   int
}

初始化方式:

intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1}  (B)
intr := Interval{end:5}           (C)

在(A)中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。(B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。
下图说明了结构体类型实例和一个指向它的指针的内存布局:

type Point struct { x, y int }

使用 new 初始化:

结构(struct)与方法(method)

作为结构体字面量初始化:

结构(struct)与方法(method)
类型 strcut1 在定义它的包 pack1 中必须是唯一的,它的完全类型名是:pack1.struct1。

下面的例子 Listing 10.2—person.go 显示了一个结构体 Person,一个方法,方法有一个类型为 *Person 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:

package main
import (
    "fmt"
    "strings"
)

type Person struct {
    firstName   string
    lastName    string
}

func upPerson(p *Person) {
    p.firstName = strings.ToUpper(p.firstName)
    p.lastName = strings.ToUpper(p.lastName)
}

func main() {
    // 1-struct as a value type:
    var pers1 Person
    pers1.firstName = "Chris"
    pers1.lastName = "Woodward"
    upPerson(&pers1)
    fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)

    // 2—struct as a pointer:
    pers2 := new(Person)
    pers2.firstName = "Chris"
    pers2.lastName = "Woodward"
    (*pers2).lastName = "Woodward"  // 这是合法的
    upPerson(pers2)
    fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)

    // 3—struct as a literal:
    pers3 := &Person{"Chris","Woodward"}
    upPerson(pers3)
    fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}

输出

The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD
The name of the person is CHRIS WOODWARD

在上面例子的第二种情况中,可以直接通过指针,像 pers2.lastName=”Woodward” 这样给结构体字段赋值,没有像 C++ 中那样需要使用 -> 操作符,Go 会自动做这样的转换。

注意也可以通过解指针的方式来设置值:(*pers2).lastName = “Woodward”

结构体的内存布局

Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。下面的例子清晰地说明了这些情况:

type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }

结构(struct)与方法(method)

递归结构体

结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 su,树中的 ri 和 le 分别是指向别的节点的指针。

链表:

结构(struct)与方法(method)
这块的 data 字段用于存放有效数据(比如 float64),su 指针指向后继节点。

Go 代码:

type Node struct {
    data    float64
    su      *Node
}

链表中的第一个元素叫 head,它指向第二个元素;最后一个元素叫 tail,它没有后继元素,所以它的 su 为 nil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。

同样地可以定义一个双向链表,它有一个前趋节点 pr 和一个后继节点 su:

type Node struct {
    pr      *Node
    data    float64
    su      *Node
}

二叉树:

结构(struct)与方法(method)

二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(root),底层没有子节点的节点叫叶子节点(leaves),叶子节点的 le 和 ri 指针为 nil 值。在 Go 中可以如下定义二叉树:

type Tree strcut {
    le      *Tree
    data    float64
    ri      *Tree
}

结构体转换

Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。

package main
import "fmt"

type number struct {
    f float32
}

type nr number   // alias type

func main() {
    a := number{5.0}
    b := nr{5.0}
    // var i float32 = b   // compile-error: cannot use b (type nr) as type float32 in assignment
    // var i = float32(b)  // compile-error: cannot convert b (type nr) to type float32
    // var c number = b    // compile-error: cannot use b (type nr) as type number in assignment
    // needs a conversion:
    var c = number(b)
    var d = number(c)
    fmt.Println(a, b, c, d)
}

输出:

{5} {5} {5} {5}

转载地址

https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/10.1.md