有些数据会有相关性,例如,一个 XY 平面上的点可以使用 (x, y) 座标来表示;名称、邮件地址、电话可能代表着一张名片上的信息。将相关联的数据组织在一起,对于数据本身的可用性或者是代码的可读性,都会有所帮助。
struct 组织数据
Go 语言中有struct
,可以用来将相关的数据组织在一起,如果你学过 C 语言,这对你应该不陌生。举个例子来说,相对于个别地访问x
、y
变量:
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}
}
若x
与y
变量,相当于 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
定义了一个结构,当中包括了x
与y
两个值域(field),接着马上用它来创建了一个实例,依顺序指定了x
与y
的值是10
与20
,可以看到,想要访问结构的值域,可以运过点运算符(.
)。
基于结构定义新类型
上面的例子中,创建了一个匿名类型的结构,你可以使用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
实例的指针值,并传递给point2
,point2
的类型是*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}
这类的写法,可以同时指定结构的值域。