结构入门


有些数据会有相关性,例如,一个 XY 平面上的点可以使用 (x, y) 座标来表示;名称、邮件地址、电话可能代表着一张名片上的信息。将相关联的数据组织在一起,对于数据本身的可用性或者是代码的可读性,都会有所帮助。

struct 组织数据

Go 语言中有struct,可以用来将相关的数据组织在一起,如果你学过 C 语言,这对你应该不陌生。举个例子来说,相对于个别地访问xy变量:

package main

import "fmt"

func main() {
    x := 10
    y := 20
    fmt.Printf("{%d %d}\n", x, y) // {10 20}

    x := 20
    y := 30
    fmt.Printf("{%d %d}\n", x, y) // {20 30}
}

xy变量,相当于 XY 平面上的 (x, y) 座标,那么将之组织在一起同时访问会比较好:

package main

import "fmt"

func main() {
    point := struct{ x, y int }{10, 20}
    fmt.Printf("{%d %d}\n", point.x, point.y) // {10 20}

    point.x = 20
    point.y = 30

    fmt.Printf("{%d %d}\n", point.x, point.y) // {20 30}
}

实际上,fmt.Println可以直接处理struct,因此,上面的例子,可以直接使用fmt.Println(point)来得到相同的显示结果。

在上面的例子中,struct定义了一个结构,当中包括了xy两个值域(field),接着马上用它来创建了一个实例,依顺序指定了xy的值是1020,可以看到,想要访问结构的值域,可以运过点运算符(.)。

基于结构定义新类型

上面的例子中,创建了一个匿名类型的结构,你可以使用type基于struct来定义新类型,例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    point1 := Point{10, 20}
    fmt.Println(point1) // {10 20}

    point2 := Point{Y: 20, X: 30}
    fmt.Println(point2) // {30 20}
}

在上面基于结构定义了新类型Point,留意到名称开头的大小写,若是大写的话,就可以在其他包中访问,这点对于结构的值域也是成立,大写名称的值域,才可以在其他包中访问。在范例中也可以看到,创建并指定结构的值域时,可以直接指定值域名称,而不一定要按照定义时的顺序。

如果一开始不知道结构的值域数值为何,可以使用var定义即可,那么值域会依类型而有适当的默认值。例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {  
    var point Point
    fmt.Println(point)      // {0 0}           
}

point并不是参考,point的位置开始,有一片可以存储结构的空间,可以使用&来获取point的地址值,point的地址值无法改变。

结构与指针

如果你创建了一个结构的实例,并将之指定给另一个结构变量,那么会进行值域的复制。例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {  
    point1 := Point{X: 10, Y: 20}
    point2 := point1

    point1.X = 20

    fmt.Println(point1)  // {20, 20}
    fmt.Println(point2)  // {10 20}
}

这对于函数的参数传递也是一样的:

package main

import "fmt"

type Point struct {
    X, Y int
}

func changeX(point Point) {
    point.X = 20
    fmt.Println(point)
}

func main() {
    point := Point{X: 10, Y: 20}

    changeX(point)     // {20 20}
    fmt.Println(point) // {10 20}
}

point的位置开始存储了结构,可以对point使用&取值,将地址值指定给指针,因此若指定或传递结构时,不是想要复制值域,可以使用指针。例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    point1 := Point{X: 10, Y: 20}
    point2 := &point1

    point1.X = 20

    fmt.Println(point1) // {20, 20}
    fmt.Println(point2) // &{20 20}
}

注意到point2 := &point1多了个&,这获取了point1实例的指针值,并传递给point2point2的类型是*Point,也就是相当于var point2 *Point = &point1,因此,当你透过point1.X改变了值,透过point2就能获取对应的改变。

类似地,也可以在传递参数给函数时使用指针:

package main

import "fmt"

type Point struct {
    X, Y int
}

func changeX(point *Point) {
    point.X = 20
    fmt.Printf("&{%d %d}\n", point.X, point.Y)
}

func main() {
    point := Point{X: 10, Y: 20}

    changeX(&point)    // &{20 20}
    fmt.Println(point) // {20 20}
}

可以看到在 Go 语言中,即使是指针,也可以直接透过点运算符来访问值域,这是 Go 提供的语法糖,point.X在编译过后,会被转换为(*point).X

你也可以透过new来创建结构实例,这会返回结构实例的地址:

package main

import "fmt"

type Point struct {
    X, Y int
}

func default_point() *Point {
    point := new(Point)
    point.X = 10
    point.Y = 10
    return point
}

func main() {
    point := default_point()
    fmt.Println(point) // &{10 10}
}

在这边,point是个指针,也就是*Point类型,存储了结构实例的地址。

结构的值域也可以是指针类型,也可以是结构自身类型之指针,因此可实现链状参考,例如:

package main

import "fmt"

type Point struct {
    X, Y int
}

type Node struct {
    point *Point
    next  *Node
}

func main() {
    node := new(Node)

    node.point = &Point{10, 20}
    node.next = new(Node)

    node.next.point = &Point{10, 30}

    fmt.Println(node.point)      // &{10 20}
    fmt.Println(node.next.point) // &{10 30}
}

$T{}的写法与new(T)是等效的,使用&Point{10, 20}这类的写法,可以同时指定结构的值域。


展开阅读全文