[Go语言入门] 12 Go语言结构体(struct)详解

  • Post author:
  • Post category:其他




12 Go语言结构体(struct)详解

Go语言中的结构体是用户自定义类型。

结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型。每个变量都叫做结构体的成员。

如果一个结构体的成员变量名是大写开头的,那么这个变量是可导出的,可以在其他包中访问。

Go语言中的结构体是值类型。



12.1 结构体类型定义

使用下面的语法定义一个结构体类型:

type identifier struct {
    field1 T1
    filed2 T2
    ...
}

示例:

type Point struct {
    X float32
    Y float32
}



12.2 创建结构体实例

有两种方式



普通方式
// a是变量名,T是一个结构体类型
var a T

本方式会给变量a分配结构体T的内存,并初始化结构体的各个数据项为零值。

示例:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p Point
    fmt.Println(p)			// 输出:{0 0}
}

使用本方式时,还支持通过结构体字面量初始化结构体。

// a是一个T类型变量,T是一个结构体类型

var a = T{value1, value2, ...}var a = T{Name1: value1, Name2: value2, ...}var a = T{
    Name1:value1,
    Name2:value2,
    ...,
}

示例:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p = Point{10, 5}
    fmt.Println(p)			// 输出:{10 5}
    
    p = Point{X:10,Y:5}
    fmt.Println(p)			// 输出:{10 5}
    
    // 如果字面量中没有对字段赋值,那么字段将会是零值
    p = Point{}
    fmt.Println(p)			// 输出:{0 0}
}

使用结构字面量时,还支持取得该字面量的地址:

// a是一个*T类型的指针,T是一个结构体类型
var a = &T{value1, value2, ...}var a = &T{Name1: value1, Name2: value2, ...}var a = &T{
    Name1:value1,
    Name2:value2,
    ...,
}

实例:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p = &Point{10, 5}
    fmt.Println(p)			// 输出:&{10 5}
    
    p = &Point{X:10,Y:5}
    fmt.Println(p)			// 输出:&{10 5}
    
    // 如果字面量中没有对字段赋值,那么字段将会是零值
    p = &Point{}
    fmt.Println(p)			// 输出:&{0 0}
}



new(T)函数创建结构体实例

使用new(T)函数为一个结构体分配内存,返回该结构体的指针。

// a是一个*T类型的指针,T是一个结构体类型
var a *T = new(T)

本方式会分配一个结构体,初始化结构体各个数据项为零值,然后把该结构体的指针赋值给变量a。

示例:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p *Point = new(Point)
    fmt.Println(*p)			// 输出:{0 0}
}



12.3 访问结构体字段



访问结构体的字段

通过

结构体.成员名

来访问结构体的字段。

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p = Point{X:10,Y:5}
    
    fmt.Println(p.X)			// 输出:10
    fmt.Println(p.Y)			// 输出:5
    
    p.X = p.X + 10
    p.Y = p.Y + 10
    
    fmt.Println(p.X)				// 输出:20
    fmt.Println(p.Y)			// 输出:15
}



通过结构体指针访问其字段
package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p *Point = new(Point)
    fmt.Println(*p)			// 输出:{0 0}
    
    (*p).X = 10
    (*p).Y = 5
    
    fmt.Println((*p).X)		// 输出:10
    fmt.Println((*p).Y)		// 输出:5
}

通过结构体指针来访问字段时,也可以省略*操作符,此时Go语言将根据代码上下文自动推断出这是一个指针解引用操作:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    var p *Point = new(Point)
    fmt.Println(*p)			// 输出:{0 0}
    
    p.X = 10
    p.Y = 5
    
    fmt.Println(p.X)		// 输出:10
    fmt.Println(p.Y)		// 输出:5
}



12.4 结构体类型的内存布局

在Go语言中,结构体和它所包含的数据在内存中是连续存储的。

对于前面所定义的Point结构体,它在内存中的结构如下图:

在这里插入图片描述

对于使用new(Point)得到的结构体指针,它在内存中的结构如下图:

在这里插入图片描述



12.5 结构体作为函数参数

由于结构体是值类型,因此使用结构体作为函数参数时,将会拷贝整个结构体的数据。

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    p := Point{10, 20}
    fmt.Println("before call myFunction: ", p)		// before call myFunction:  {10 20}
    myFunction(p)
    fmt.Println("after call myFunction: ", p)		// after call myFunction:  {10 20}
}

func myFunction(p Point) {
    fmt.Println("enter myFunction: ", p)			// enter myFunction:  {10 20}
    
    p.X += 10
    p.Y += 10
    
    fmt.Println("exit myFunction: ", p)				// exit myFunction:  {20 30}
}

如果使用结构体指针作为函数参数,那么在函数中对结构体的修改将会反映到调用者中的结构体。

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func main() {
    p := Point{10, 20}
    fmt.Println("before call myFunction: ", p)		// before call myFunction:  {10 20}
    myFunction(&p)
    fmt.Println("after call myFunction: ", p)		// after call myFunction:  {20 30}
}

func myFunction(p *Point) {
    fmt.Println("enter myFunction: ", p)			// enter myFunction:  &{10 20}
    
    p.X += 10
    p.Y += 10
    
    fmt.Println("exit myFunction: ", p)				// exit myFunction:  &{20 30}
}



12.6 结构体嵌套和匿名成员



结构体嵌套

结构体的成员可以是结构体。这称作结构体嵌套。例如:

type Point struct {
    X float32
    Y float32
}

type Circle struct {
    Point Point
    R float32
}

func main() {
    c := Circle{
        Point: Point{10, 5},
        R: 2,
    }
    
    fmt.Println(c)		// 输出 {{10 5} 2}
}



匿名成员

Go还允许我们定义不带名称的结构体成员,只需要指定类型即可。这中成员成员匿名成员。匿名成员的类型必须是一个命名类型或命名类型的指针。

例如:

type Point struct {
    X float32
    Y float32
}

type Circle struct {
    Point				// 匿名成员,实际上等价于 Point Point
    R float32
}

有了上面的定义之后,我们可以直接通过Circle访问Point的成员X和Y,就好像他们是Circle自身的成员一样。

c := Circle{
    	Point:Point{X:10, Y:5},
		R:2,
	 }
fmt.Println(c)
c.X += 10				// 实际上等价于 c.Point.X += 10
c.Y += 10				// 实际上等价于 c.Point.Y += 10
fmt.Println(c)



12.7 结构体比较

如果结构体的所有成员变量都是可比较的,那么这个结构体就是可比较的。两个结构体可以通过==或!=运算符进行比较。==操作符按照顺序比较两个结构体的成员变量。

示例:

type Person struct {
    Name string
    Age int
}

func main() {
    p := Person{"jason", 25}
    q := Person{"jason", 25}
    
    fmt.Println(p == q)				// true
}



12.8 结构体方法



先看一个例子

先让我们看一个例子:

package main

import "fmt"

// 圆
type Circle struct {
    R float32
}

// 正方形
type Square struct {
    W float32
    H float32
}

// 计算圆面积
func GetCircleArea(c Circle) float32 {
    return 3.14 * c.R * c.R
}

// 计算正方形面积
func GetSquareArea(s Square) float32 {
    return s.W * s.H
}

func main() {
    c := Circle{10}
    area := GetCircleArea(c)
    fmt.Println(area)				// 输出: 314
    
    s := Square{10, 20}
    area = GetSquareArea(s)
    fmt.Println(area)				// 输出:200
}

上面的例子中,定义了两个函数,分别用于计算圆的面积和正方形的面积。我们是否可以把上面的GetCircleArea和GetSquareArea这两个函数定义为结构体的成员呢?答案是可以。要通过Go语言的方法来实现。



方法声明和调用

方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上。

Go语言的方法就是一个绑定了接收者的函数,接收者实际上是该函数的隐含参数。

方法声明语法:

// var_name是接收者变量名,T是接收者类型,method_name是方法名,param_list是参数列表,return_list是返回值列表
func (var_name T) method_name(param_list) [return_list]{
   /*函数体*/
}

如果方法名是以大写开头的,那么该方法是可导出的,在其他包中也可访问。否则该方法只能在它所属的包内部访问。

方法调用语法:

values = var_name.method_name(param_list)

示例(为结构体定义方法):

package main

import (
   "fmt"  
)

// 圆
type Circle struct {
    R float32
}

// 正方形
type Square struct {
    W float32
    H float32
}

// 计算圆面积
func (c Circle)GetArea() float32 {
    return 3.14 * c.R * c.R
}

// 计算正方形面积
func (s Square)GetArea(s Square) float32 {
    return s.W * s.H
}

func main() {
    c := Circle{10}
    area := c.GetArea()
    fmt.Println(area)				// 输出: 314
    
    s := Square{10, 20}
    area = s.GetArea()
    fmt.Println(area)				// 输出:200
}

从上面的代码可以看出,为不同类型定义的方法是可以重名的,调用方法时,Go编译器通过结构体变量的类型推断出实际调用的是哪个方法。

方法很像其他语言(比如Java)中的类的成员方法。



指针类型作为方法接收者

定义方法时,接受者可以是指针类型。

由于接收者实质上是函数的隐含参数,其传递机制同函数参数的传递机制时一样的,方法中见到的接收者变量是主调函数中该变量的副本。而如果接受者变量是指针类型,方法中见到的接收者变量是该指针的副本,指针的副本与原指针指向的是同一个值。

下面让我们看一个例子:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func (p Point) movX_1(dx float32) {
    p.X += dx
}

func (p *Point) movX_2(dx float32) {
    p.X += dx
}

func main() {
    p := Point{10, 10}
    fmt.Println(p)		// {10 10}
    
    p.movX_1(10)
    fmt.Println(p)		// {10 10}
    
    (&p).movX_2(10)
    fmt.Println(p)		// {20 10}
}

由上面的例子看以看出接受者变量是指针类型时的不同。

对于接收者类型是*T的方法,可以通过类型T的接收者直接调用,不必非要通过类型*T的接收者来调用。Go语言推断出此处需要一个指针,在调用方法前自动对接收者做取地址操作。例如:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func (p Point) movX_1(dx float32) {
    p.X += dx
}

func (p *Point) movX_2(dx float32) {
    p.X += dx
}

func main() {
    pp := &Point{10, 10}
    pp.movX_2(10)
    fmt.Println(pp)		// &{20 10}
    
    p := Point{10, 10}
    p.movX_2(10)		// 通过T类型接收者也可调用*T类型的方法,此处会自动取地址。与(&p).movX_2(10)效果一样
    fmt.Println(p)		// {20 10}
}

但是,调用接收者是*T的方法时,接收者必须是可取地址的,如果接收者不是一个可取地址的表达式,那么会编译失败。比如,当接收者是一个字面量时,由于字面量是无法取地址的,会编译失败:

package main

type Point struct {
    X float32
    Y float32
}

func (p Point) movX_1(dx float32) {
    p.X += dx
}

func (p *Point) movX_2(dx float32) {
    p.X += dx
}

func main() {
    Point{10, 10}.movX_1(10)
    
    Point{10, 10}.movX_2(10)		// 编译错误:cannot call pointer method on Point literal
}

对于接受者类型是T的方法,即可以通过T类型接收者调用,也可以通过*T类型接收者来调用。编译器推断出此处需要的是T类型,在调用方法前自动对*T类型接收者做解引用操作。例如:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func (p Point) movX_1(dx float32) {
    p.X += dx
}

func (p *Point) movX_2(dx float32) {
    p.X += dx
}

func main() {
    p := Point{10, 10}
    p.movX_1(10)
    fmt.Println(p)		// {10 10}
    
    pp := &Point{10, 10}
    pp.movX_1(10)		// 通过*T类型接收者也可以调用T类型的方法,此处自动对*T类型解引用。与(*pp).movX_1(10)效果一样。
    fmt.Println(pp)		// &{10 10}
}

综上所述:

  • 不论方法是通过T类型还是*T类型的接收者来定义的,均可通过T类型或*T类型的接收者来调用该方法。
  • 如果方法是通过*T类型定义的,调用时的接收者必须是可被取地址的,否则编译就发生错误。
  • 方法调用时,接收者到底是按T类型传递还是按*T类型传递,是由方法定义时的接收者类型决定的,不是由调用时使用的接收者类型决定的。



接收者除了可以时结构体,还可以是任何自定义的类型

Go语言不光可以为结构体类型定义方法,对于任何自定义的类型都可以定义方法。但不可为内置类型定义方法,比如不可以为int、string、slice、map定义方法。下面是一个例子:

package main

import "fmt"

type Speed float32

type MyDict map[string]int

func (s Speed)myMethod() {
	fmt.Println("myMethod for Speed")
}

func (d MyDict)myMethod() {
	fmt.Println("myMethod for MyDict")
}

func main() {
	var a = Speed(6)
	var b = MyDict{"hello":15}
	
	a.myMethod()			// myMethod for Speed
	b.myMethod()			// myMethod for MyDict
}



接收者可以是nil值

如果接收者是引用类型(切片、Map、接口、*T),那么接收者可以为nil。这和其他面向对象的语言不同,比如Java或C++中的this变量一定不为空。

下面是一个例子:

package main

import "fmt"

type Point struct {
    X float32
    Y float32
}

func (p *Point)mov(dx, dy float32) {
    if p == nil {
        fmt.Println("p is nil")
        return
    }
    p.X += dx
    p.Y += dy
}

func main() {
    p := Point{10, 10}
    p.mov(2, 4)
    fmt.Println(p)			// 输出:{12 14}
    
    var pp *Point = nil
    pp.mov(2, 4)			// 输出:p is nil
}



结构体匿名成员的方法可直接用在结构体上

比如下面的结构体:

type Point struct {
    X, Y float32
}

func (p *Point)mov(dx, dy float32) {
    p.X += dx
    p.Y += dy
}

type Circle struct {
    Point
    R float32
}

Point结构体的mov方法可以直接用在Circle结构体上,就好像它Circle结构体自己的方法一样。

c := Circle{Point:Point{10, 10}, R:2}
c.mov(2, 3)
fmt.Println(c)			// 输出: {{12 13} 2}

注意:只有匿名成员的方法可用在外层结构体上,非匿名成员的方法必须通过成员变量来调用。比如:

type Point struct {
    X, Y float32
}

func (p *Point)mov(dx, dy float32) {
    p.X += dx
    p.Y += dy
}

type Circle struct {
    Point Point				// 此处的Point并非匿名的
    R float32
}

func main() {
    c := Circle{Point:Point{10, 10}, R:2}
    
	// c.mov(2, 3)			// 编译错误:c.mov undefined (type Circle has no field or method mov)
    
    // 必须通过c.Point来调用mov方法
    c.Point.mov(2, 3)
}

注意:外层结构体可以直接调用匿名成员的方法看起来很像是其他面向对象语言中的继承,但Go语言并没有继承的概念,Go语言中结构体的成员组合成结构体,是一种聚合关系。通过外层结构体直接使用匿名成员的方法,实际上是Go语言提供的语法糖,类似

c.mov(2, 3)

这样的代码,实际上被编译器翻译为:

c.Point.mov(2, 3)

是Go编译器做了很多自动处理,使得程序员可以少写一些代码。


Copyright@2022, 359152155@qq.com