8.结构体

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方法

本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

posted @ 2025-08-11 23:51  Surpassme  阅读(11)  评论(0)    收藏  举报