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
}
根据以上代码运行结果,我们来总结一下结构体与匿名结构体主要区别,如下所示:

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,或扫描下面的二维码添加关注:

作者: Surpassme
来源: http://www.jianshu.com/u/28161b7c9995/
http://www.cnblogs.com/surpassme/
声明:本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接 ,否则保留追究法律责任的权利。如有问题,可发送邮件 联系。让我们尊重原创者版权,共同营造良好的IT朋友圈。

浙公网安备 33010602011771号