8.结构体

作者: Surpassme | 来源:发表于2025-08-10 23:46 被阅读0次

8.结构体

    Go语言的结构体有点像面向对象编程语言中的"类",但也不完全是。

面向对象是一种对现实世界理解和抽象的一种方法,通过抽象把相关的数据和方法组织为一个整体来看待,从高层次进行系统建模。更贴近事物的自然运行模式。例如:人类就是一个抽象的类,而小明,则代表了一个具体人。在其他语言中,类一般使用关键字class来定义一个类。在Go语言中,没有class关键字,而是使用结构体替换了

8.1 结构体定义

    结构体使用关键字type定义,也可以把结构体当成类型使用。在定义时,必须指定结构的字段名称(属性)和类型,定义的基本语法如下所示:

type name struct {
    field1 dataType
    field2 dataType
    ...
}

    各字段解释如下所示:

  • type: 用于设置当前自定义的变量为自定义类型
  • name: 结构体名字,满足标识符定义规则即可
  • struct: 声明当前类型为结构体类型
  • fields: 结构体字段名称,也称为结构成员
  • dataType: 字段的数据类型

8.2 结构体初始化

    初始化类似于面向对象语言中的实例,即从抽象类中生成一个具体的对象。示例代码如下所示:

package main

import "fmt"

// 定义结构体
type User struct {
    id         int
    name, addr string
    height     float32
}

func main() {
    // 使用var声明,非常常用
    var u1 User
    // 加上字段打印
    fmt.Printf("u1: %+v\n", u1)
    // 加上打印更多信息
    fmt.Printf("u1: %#v\n", u1)

    // 字面量初始化
    u2 := User{}
    fmt.Printf("u2: %#v\n", u2)

    // 字面量初始化,为字段进行赋值
    u3 := User{name: "Surpass"}
    fmt.Printf("u3: %#v\n", u3)

    // 字面量初始化,为字段进行赋值,名称对应,或忽略顺序
    u4 := User{
        name:   "Surpass",
        height: 1.85,
        id:     9999,
        addr:   "Shanghai",
    }
    fmt.Printf("u4: %#v\n", u4)

    // 使用new方法
    u5 := new(User)
    fmt.Printf("u5: %#v\n", u5)

    // 获取结构体实例化的内存地址
    u6 := &User{}
    fmt.Printf("u5: %#v\n", u6)
}

    代码运行结果如下所示:

u1: {id:0 name: addr: height:0}
u1: main.User{id:0, name:"", addr:"", height:0}
u2: main.User{id:0, name:"", addr:"", height:0}
u3: main.User{id:0, name:"Surpass", addr:"", height:0}
u4: main.User{id:9999, name:"Surpass", addr:"Shanghai", height:1.85}
u5: &main.User{id:0, name:"", addr:"", height:0}
u5: &main.User{id:0, name:"", addr:"", height:0}

    结构体的可见性如下所示:

  • 结构体名称中首字母大写,则跨包可见,否则仅本包内可见
  • 结构体成员中首字母大写,则跨包可见

8.3 结构体访问与修改

    结构体访问,可以使用字段名称访问,修改可以通过字段名称进行修改。示例代码如下所示:

package main

import "fmt"

// 定义结构体
type User struct {
    id         int
    name, addr string
    height     float32
}

func main() {
    u := User{
        name:   "Surpass",
        height: 1.85,
        id:     9999,
        addr:   "Shanghai",
    }
    fmt.Printf("u4: %#v\n", u)
    // 访问结构体字段
    fmt.Printf("修改前访问User's id is: %d name is: %s  addr is: %s, height is: %f\n", u.id, u.name, u.addr, u.height)

    // 修改结构体字段
    u.id = 1000
    u.height = 1.75
    u.name = "Surmount"
    fmt.Printf("修改后访问User's id is: %d name is: %s addr is: %s, height is: %f\n", u.id, u.name, u.addr, u.height)
}

    运行结果如下所示:

u4: main.User{id:9999, name:"Surpass", addr:"Shanghai", height:1.85}
修改前访问User's id is: 9999 name is: Surpass  addr is: Shanghai, height is: 1.850000
修改后访问User's id is: 1000 name is: Surmount addr is: Shanghai, height is: 1.750000

8.4 结构体指针方式的初始化

    在初始化结构体时,可以使用内置方法new& 两种方式,这两种初始化方法都是由指针方式完成的。在访问结构体字段时,使用点,而编译器会自动将其转换为(structName).field的形式访问。不同的初始化方式在使用上存在一定的差异,但为了统一使用方式,常规初始化的使用方法也能兼容指针方式*。指针初始化的真正使用方式如下所示:

package main

import "fmt"

// 定义结构体
type User struct {
    id         int
    name, addr string
    height     float32
}

func main() {
    // 使用new方法初始化
    var u1 *User = new(User)
    (*u1).name = "Surpass"
    (*u1).addr = "Shanghai"
    fmt.Printf("u1's name:%+v,addr:%+v,u1:%#v\n", u1.name, u1.addr, u1)

    // 通过 & 初始化
    var u2 *User = &User{}
    (*u2).name = "Surmount"
    (*u2).addr = "Wuhai"
    fmt.Printf("u2's name:%+v,addr:%+v,u2:%#v\n", u2.name, u2.addr, u2)
}

    代码运行结果如下所示:

u1's name:Surpass,addr:Shanghai,u1:&main.User{id:0, name:"Surpass", addr:"Shanghai", height:0}
u2's name:Surmount,addr:Wuhai,u2:&main.User{id:0, name:"Surmount", addr:"Wuhai", height:0}

    根据以上代码总结如下所示:

  • 使用内置方法new和&初始化结构体时,其实例都是指针类型
  • 通过结构体实例u1和u2访问结构体字段时,需要先使用取值操作符从结构体实例存储的内存地址获取结构体,再从结构体获取相应的字段,再进行取值或赋值操作
  • 指针方式的初始化结构也允许直接使用点访问结构体字段,因为编译器会转换为(*structName).field

8.5 结构体标签

    在定义一个结构体,我们还可以为每个字段添加标签(tag),它是一个附属于字段的字符串,用于标识字段的一些属性。例如JSON、ORM框架等用得非常多,基本语法如下所示:

type name struct {
    field1 dataType `key1:"value1" key2:"value2"`
    field2 dataType `key1:"value1" key2:"value2"`
}
  • 标签位于字段的数据类型之后,以字符串表示,用反引号包裹
  • 标签内容可以由一个或多个键值对组成,键值之间使用冒号分隔,且不能留有空格
  • 标签内容中的值使用双引号包裹,多个键值对之间使用空格分隔

    先来看看示例代码1:

package main

import (
    "encoding/json"
    "fmt"
)

// 定义结构体
type User struct {
    id         int
    name, addr string
    height     float32
}

func main() {
    u1 := User{
        id:     9999,
        name:   "Surpass",
        addr:   "Shanghai",
        height: 1.89,
    }
    if data, err := json.Marshal(u1); err == nil {
        fmt.Println(string(data))
    }
}

    运行结果如下所示:

{}

    以上代码输出结果为空,VSCode出还检查出来问题,如下所示:

struct type 'surpass.net.User' doesn't have any exported fields, nor custom marshaling (SA9005)

    从以下提示信息,可以看出结构体中的字段首字母都是小写,因此无法导出相应的标识符,导致encoding/json无法获取结构体中的字段数据。那解决办法,将首字母改成大写即可,但JSON数据中的key一般是小写,这个时候就可以使用结构体标签,改造后的代码如下所示:

package main

import (
    "encoding/json"
    "fmt"
)

// 定义结构体
type User struct {
    Id     int     `json:"id"`
    Name   string  `json:"name"`
    Addr   string  `json:"addr"`
    Height float32 `json:"height"`
}

func main() {
    u1 := User{
        Id:     9999,
        Name:   "Surpass",
        Addr:   "Shanghai",
        Height: 1.89,
    }
    if data, err := json.MarshalIndent(u1, "", "  "); err == nil {
        fmt.Println(string(data))
    }
}

    最终运行结果如下所示:

{
  "id": 9999,
  "name": "Surpass",
  "addr": "Shanghai",
  "height": 1.89
}

8.6 匿名结构体

    匿名结构体类似于匿名函数,在使用匿名结构体时,需要将其赋值给变量。使用方法如下所示:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    // 定义匿名结构体
    var User struct {
        Id     int     `json:"id"`
        Name   string  `json:"name"`
        Addr   string  `json:"addr"`
        Height float32 `json:"height"`
    }

    User.Id = 9999
    User.Name = "Surpass"
    User.Addr = "Shanghai"
    User.Height = 1.89
    fmt.Printf("User value:%#v\n", User)
    if data, err := json.MarshalIndent(User, "", "  "); err == nil {
        fmt.Println(string(data))
    }

    // 定义匿名结构体,并赋值
    u := struct {
        Id     int     `json:"id"`
        Name   string  `json:"name"`
        Addr   string  `json:"addr"`
        Height float32 `json:"height"`
    }{
        Id:     8888,
        Name:   "Surmount",
        Addr:   "Wuhan",
        Height: 1.95,
    }
    fmt.Printf("User value:%#v\n", u)
    if data, err := json.MarshalIndent(u, "", "  "); err == nil {
        fmt.Println(string(data))
    }
}

    运行结果如下所示:

User value:struct { Id int "json:\"id\""; Name string "json:\"name\""; Addr string "json:\"addr\""; Height float32 "json:\"height\"" }{Id:9999, Name:"Surpass", Addr:"Shanghai", Height:1.89}
{
  "id": 9999,
  "name": "Surpass",
  "addr": "Shanghai",
  "height": 1.89
}
User value:struct { Id int "json:\"id\""; Name string "json:\"name\""; Addr string "json:\"addr\""; Height float32 "json:\"height\"" }{Id:8888, Name:"Surmount", Addr:"Wuhan", Height:1.95}
{
  "id": 8888,
  "name": "Surmount",
  "addr": "Wuhan",
  "height": 1.95
}

    根据以上代码运行结果,我们来总结一下结构体与匿名结构体主要区别,如下所示:

0801-结构体与匿名结构区别.png

8.7 匿名字段

    匿名字段是指在结构体中没有明确定义字段名称,只定义了字段的数据类型。在访问时,可以通过字段的数据类型进行访问。示例代码如下所示:

package main

import (
    "fmt"
)

// 定义匿名字段
type User struct {
    int
    string
    float32
    bool
}

func main() {
    u1 := User{}
    fmt.Printf("u1 value: %#v\n", u1)
    u2 := User{9999, "Surpass", 1.90, false}
    fmt.Printf("结构体匿名字段int值:%v\n", u2.int)
    fmt.Printf("u2 value: %#v\n", u2)
    fmt.Printf("结构体匿名字段string值:%v\n", u2.string)
    fmt.Printf("结构体匿名字段float32值:%v\n", u2.float32)
    fmt.Printf("结构体匿名字段bool值:%v\n", u2.bool)

    u2.bool = true
    fmt.Printf("结构体匿名字段bool值:%v\n", u2.bool)
}

    运行结果如下所示:

u1 value: main.User{int:0, string:"", float32:0, bool:false}
结构体匿名字段int值:9999
u2 value: main.User{int:9999, string:"Surpass", float32:1.9, bool:false}
结构体匿名字段string值:Surpass
结构体匿名字段float32值:1.9
结构体匿名字段bool值:false
结构体匿名字段bool值:true

如果结构体中存在匿名字段,则同一种数据类型不允许存在多个,如下所示:

type User struct {
    int
    string
    string //会报错,不允许存在两个string类型,必须类型不一样才能区分
    float32
    bool
}

8.8 结构体嵌套

    在面向对象里面有一个设计原则组合优于继承,而在Go语言中使用嵌套则很好的体现了这一种原则。因为结构体的字段可以设置不同的数据类型,而struct本身也是一种数据类型。因此也可以在一个结构体中使用另一个结构体做为字段,从而形成一种递进的关系,形成嵌套。这种也称之为结构体嵌套,通过这种方式也可以实现面向对象语言中的继承。示例如下所示:

type User struct {
    Id int
    Name string
    Addr string
    Height float32
}

type Human struct {
    User
    Gender byte
}

    示例代码如下所示:

package main

import "fmt"

type User struct {
    Id     int
    Name   string
    Addr   string
    Height float32
}

type Human struct {
    User   User
    Gender byte
    Name   string
}

func main() {
    // 嵌套结构体初始化方式一
    u := User{
        Id:     9999,
        Name:   "Surpass",
        Addr:   "Shanghai",
        Height: 1.89,
    }
    h := Human{
        User:   u,
        Gender: 0,
        Name:   "Human",
    }
    fmt.Printf("h value: %#v\n", h)
    fmt.Printf("h name value: %v,u name value:%v\n", h.Name, h.User.Name)

    // 嵌套结构体和初始化方式二:
    h2 := Human{
        User: User{
            Id:     8888,
            Name:   "Surpass",
            Addr:   "Wuhan",
            Height: 1.99,
        },
        Gender: 1,
        Name:   "Surmount",
    }
    fmt.Printf("h2 value: %#v\n", h2)
    fmt.Printf("h2 name value: %v,u name value:%v\n", h2.Name, h2.User.Name)
}

    运行结果如下所示:

h value: main.Human{User:main.User{Id:9999, Name:"Surpass", Addr:"Shanghai", Height:1.89}, Gender:0x0, Name:"Human"}
h name value: Human,u name value:Surpass
h2 value: main.Human{User:main.User{Id:8888, Name:"Surpass", Addr:"Wuhan", Height:1.99}, Gender:0x1, Name:"Surmount"}
h2 name value: Surmount,u name value:Surpass

    通过上面代码,总结如下所示:

  • 在结构体Human中嵌套了结构体User。使得Human拥有User的全部字段。若从面向对象的角度来看,Human继承了父类User
  • 如果两个结构拥有相同的字段,在访问字段,需要明确指明访问哪一个结构体的字段

    在结构体嵌套中,也可以通过匿名结构体实现。示例代码代码如下所示:

package main

import (
    "fmt"
)

type User struct {
    Id     int
    Name   string
    Addr   string
    Height float32
}

type Human struct {
    Name   string
    Gender byte
    User
    Car struct{ Color, Brand string }
}

func main() {
    h := Human{
        Name:   "Surmount",
        Gender: 1,
        User: User{
            Id:     8888,
            Name:   "Surpass",
            Addr:   "Wuhan",
            Height: 1.99,
        },
        Car: struct {
            Color string
            Brand string
        }{Color: "Red", Brand: "Audi"},
    }
    fmt.Printf("h value: %#v\n", h)
    fmt.Printf("h name value: %v,u name value:%v color value:%v\n", h.Name, h.User.Name,h.Car.Color)
}

    最终的运行结果如下所示:

h value: main.Human{Name:"Surmount", Gender:0x1, User:main.User{Id:8888, Name:"Surpass", Addr:"Wuhan", Height:1.99}, Car:struct { Color string; Brand string }{Color:"Red", Brand:"Audi"}}
h name value: Surmount,u name value:Surpass color value:Red

在对结构体进行初始化时,如果要按变量名进行赋值,要么都指定,要么全不指定,不可以混用

8.9 构造函数

    Go语言并没有从语言层面为结构体提供构造器,但有时候可以通过一个函数为结构体初始化提供默认属性值,从而更加方便得到一个结构体和实例。习惯上,函数命名以New做为开头。使用构造函数时,可以选择性为结构字段进行赋值,若未赋值,则使用相应的零值。示例代码如下所示:

package main

import "fmt"

type User struct {
    Id     int
    Name   string
    Addr   string
    Height float32
}

// 这里NewUserWithNameAndAddr返回值使用了值拷贝,会增加内存开销
func NewUserWithNameAndAddr(name, addr string) User {
    return User{Name: name, Addr: addr}
}

// 一般在返回结构体初始化值,使用指针类型,避免实例的拷贝
func NewUser(id int, name, addr string, height float32) *User {
    return &User{Id: id, Name: name, Addr: addr, Height: height}
}

func main() {
    u1 := NewUserWithNameAndAddr("Surpass", "Shanghai")
    u2 := NewUser(9999, "Surpass", "Shanghai", 1.89)
    fmt.Printf("u1:%#v\n", u1)
    fmt.Printf("u2:%#v\n", u2)
}

    运行结果如下所示:

u1:main.User{Id:0, Name:"Surpass", Addr:"Shanghai", Height:0}
u2:&main.User{Id:9999, Name:"Surpass", Addr:"Shanghai", Height:1.89}

为了减少内存开销,一般在对结构体定义构造函数时,使用指针类型

8.10 结构体Receiver

    在Go语言中,可以为任意类型包括结构体增加方法,语法形式如下所示:

func (receiver) name(parametes) returnValue{
    代码块
}
  • receiver: 方法绑定的对象,receiver必须是一个类型T实例或类型T的指针,T不能是指针或接口
  • name: 方法名称
  • parameters:参数列表
  • returnValue: 方法返回值

在Go语言中,函数与方法代表不同的概念,函数是独立的,方法一般是指绑定到结构体的方法,其依赖于结构体

    示例代码如下所示:

package main

import "fmt"

type User struct {
    Id     int
    Name   string
    Addr   string
    Height float32
}

// 将 GetUserName 绑定到结构体 User
func (u User) GetUserName() string {
    return u.Name
}

func (u *User) GetUserAddr() string {
    return u.Addr
}

func (u User) SetUserName(name string) {
    fmt.Printf("非指针Receiver修改前:%+v,%p\n", u, &u)
    u.Name = name
    fmt.Printf("非指针Receiver修改后:%+v,%p\n", u, &u)
}

func (u *User) SetUserAddr(addr string) {
    fmt.Printf("指针Receiver修改前:%+v,%p\n", u, u)
    u.Addr = addr
    fmt.Printf("指针Receiver修改后:%+v,%p\n", u, u)
}

func main() {
    u := User{Name: "Surmount", Addr: "Wuhan"}
    fmt.Printf("main函数中:%+v,%p\n", u, &u)
    u.SetUserName("Surpass")
    u.SetUserAddr("Shanghai")
    fmt.Printf("main函数中:%+v,%p\n", u, &u)
}

    运行结果如下所示:

main函数中:{Id:0 Name:Surmount Addr:Wuhan Height:0},0xc000106660
非指针Receiver修改前:{Id:0 Name:Surmount Addr:Wuhan Height:0},0xc0001066c0
非指针Receiver修改后:{Id:0 Name:Surpass Addr:Wuhan Height:0},0xc0001066c0
指针Receiver修改前:&{Id:0 Name:Surmount Addr:Wuhan Height:0},0xc000106660
指针Receiver修改后:&{Id:0 Name:Surmount Addr:Shanghai Height:0},0xc000106660
main函数中:{Id:0 Name:Surmount Addr:Shanghai Height:0},0xc000106660

    从上面示例可以看出,如果是非指针类的Receiver进行调用时,操作是副本,存在值拷贝,而指针类的Receiver进行调用时,操作的是同一个内存的同一个实例。如果是操作大内存的对象时,且操作的是同一个实例时,一定要使用指针类型的Receiver方法

相关文章

  • 结构式写作复盘

    十大结构式写作 1.日记体 2.清单体 3.语录体 4.资讯体 5.问答体 6互动体 7.图片体 8.干货体 9....

  • 结构体

    [toc] 结构体的定义方式 先定义结构体类型,再定义结构体变量 定义结构体类型的同时定义结构体变量 定义结构体类...

  • 【C语言笔记】<十九>结构体

    结构体的基本概念 结构体初始化 结构体的内存存储细节 结构体定义的方式 结构体类型的作用域 指向结构体的指针 结构...

  • C结构体和链表

    一,结构体变量定义及初始化 二,无名结构体 备注:无名结构体很少使用 三,宏定义结构体 四,结构体嵌套 五,结构体...

  • 结构体

    结构体定义* 结构体中的格式:* struch 结构体名* {* 结构体成员变量* }* 结构体中的特点* 1.结...

  • 结构体数组的定义

    结构体数组的定义 1、先定义结构体类型,再定义结构体数组 2、定义结构体类型的同时定义结构体数组 3、省略结构体类...

  • C#结构体,析构方法,跨程序访问

    结构体 结构体定义 结构体的语法格式: struct + 结构体名 { 结构体成员变量(相当于类中的字段) } 结...

  • 结构体

    结构体有名定义 无名定义 结构体嵌套定义 结构体内存对齐 结构体成员初始化 结构体变量引用 结构体的有名定义:直白...

  • 菜鸡学Swift3.0 13.结构体

    结构体 struct 是值类型 1.定义结构体 struct 结构体类型 { var 结构体属性:类型 ...} ...

  • 结构体

    结构体初识 结构体指针 结构体的匿名字段 结构体嵌套 Go语言中的OOP

网友评论

    本文标题:8.结构体

    本文链接:https://www.haomeiwen.com/subject/mppnnjtx.html