本文是Golang数据类型之结构体-上篇的续篇内容
1、结构体指针
1.1 声明
和其他基础数据类型一样,也可声明结构体指针变量,此时变量被初始化为nil
func TestMain4(t *testing.T) {
var person *Person
fmt.Println(person) // <nil>
}
1.2 声明并初始化
声明并初始化指针对象
// 先声明再初始化
//var person *Person
//person = &Person{}
// 简短声明
person := new(Person)
//person := &Person{} // *Person
fmt.Printf("%p", person) // 0xc00013a080
声明并初始化赋值
var person *Person = &Person{
Name: "andy",
Age: 66,
Gender: "male",
Weight: 120,
FavoriteColor: []string{"red", "blue"},
}
fmt.Printf("%p", person) // 0xc0000ce080
1.3 通过new函数创建指针对象
Go
中常定义N(n)ew
+结构体名命名的函数用于创建对应的结构体值对象或指针对象
person := new(Person)
fmt.Printf("%p", person) // 0xc00013a080
fmt.Printf("%T", person) // *test.Person
// 定义工厂函数用于创建Author对象
func NewAuthor(id int, name, birthday, addr, tel, desc string) *User {
return &User{id, name, birthday,addr, tel, desc}
}
// 调用
me8 := NewAuthor(1004, "geek", "2021-06-08", "北京市", "15588888888", "备注")
fmt.Printf("%T: %#v
", me8, me8)
1.4 传递结构体指针
将一个结构体的指针传递给函数,能否修改到该结构体
结果是可以修改该实例对象
func ChangeColor(car *Car) {
car.Color = "blue"
fmt.Println(car.Color)
}
func main() {
car := Car{
Color: "yellow", // 黄色
Brand: "ford", // 福特
Model: "Mustang", // 野马
}
ChangeToW(car)
fmt.Println(car.Color) // blue
}
1.5 结构体值与结构体指针
什么是值? 什么是指针?
下面三种方式都可以构造Car struct
的实例
c1 := Car{}
c2 := &Car{}
c3 := new(Car)
fmt.Println(c1, c2, c3) // { } &{ } &{ }
c1
、c2
、c3
都是car struct
的实例,c2
, c3
是指向实例的指针,指针中保存的是实例的地址,所以指针再指向实例,c1
则是直接指向实例。这三个变量与Car struct
实例的指向关系如下
变量名 指针 数据对象(实例)
-------------------------------
c1 -------------------> { }
c2 -----> ptr(addr) --> { }
c3 -----> ptr(addr) --> { }
访问实例和访问实例指针是否有区别
fmt.Println("c1, ", c1.Color) // 访问实例的属性
fmt.Println("c2, ", (*c2).Color) // 先通过*求出 指针的值,就是实例的内存地址, 然后通过实例的内存地址访问该实例对象的属性
如果我们需要访问指针对象的属性, 上面的(*c2).Color
是理论上的正确写法, 可以看出过于繁琐, 而我们方法指针,往往也是想访问这个指针的实例, 所以编译帮我们做了优化, 比如访问指针实例也可以这样写
fmt.Println("c2, ", c2.Color) // 编译器自动补充上(*c2).Color, 这样写法上就简洁了
简单总结:尽管一个是数据对象值,一个是指针,它们都是数据对象的实例。也就是说,p1.name
和p2.name
都能访问对应实例的属性,只是指针的访问写法是一种简写(正确写法由编译器补充)
1.6 传值还是传递指针
前面文章Golang函数参数的值传递和引用传递说的也是这个话题
即什么时候传值,什么时候传递指针?
- 传递值: 不希望实例被外部修改的时候,传值就相当于
copy
了一份副本给函数 - 传递指针: 希望外部能修改到这个实例本身的时候,就需要传递该实例的指针,就是把该实例的内存地址告诉对方,可以通过地址直接找到本体
但是经常看到函数接收的结构体参数都是指针是为什么
因为复制传值时,如果函数的参数是一个struct
对象,将直接复制整个数据结构的副本传递给函数,这有两个问题
- 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
- 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小
所以为了节省开销一般都会选择传递指针
2、匿名结构体
在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置
package main
import "fmt"
func main() {
var me struct {
ID int
Name string
}
fmt.Printf("%T
", me) // struct { ID int; Name string }
fmt.Printf("%#v
", me) // struct { ID int; Name string }{ID:0, Name:""}
fmt.Println(me.ID) // 0
me.Name = "geek"
fmt.Printf("%#v
", me) // struct { ID int; Name string }{ID:0, Name:"geek"}
me2 := struct {
ID int
Name string
}{1, "geek"}
fmt.Printf("%#v
", me2) // struct { ID int; Name string }{ID:1, Name:"geek"}
}
3、结构体方法
可以为结构体定义属于自己的函数
在声明函数时,声明属于结构体的函数,方法与结构体绑定,只能通过结构体person
的实例访问,不能在外部直接访问,这就是结构体方法和函数的区别,例如
// p 是person的别名
func (p Person) add() int {
return p.Age * 2
}
调用结构体方法
func TestMain6(t *testing.T) {
m := new(Person)
m.Age = 18
fmt.Println(m.add()) // 36
}
4、结构体嵌套
4.1 匿名嵌套
简单来说,就是将数据结构直接放进去,放进去的时候不进行命名
在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置
匿名结构体可以组合不同类型的数据,使得处理数据变得更为灵活。尤其是在一些需要将多个变量、类型数据组合应用的场景,匿名结构体是一个不错的选择
// 访问方式 结构体.成员名
type Person2 struct {
Name string
Age int
Gender string
Weight uint
FavoriteColor []string
NewAttr string
Addr Home
NewHome
}
type NewHome struct {
City string
}
func TestPerson2(t *testing.T) {
m := new(Person2)
m.Age = 18
m.City = "beijing"
fmt.Println(m.City) // beijing
}
嵌套过后带来的好处就是能够像访问原生属性一样访问嵌套的属性
示例
package main
import (
"encoding/json"
"fmt"
)
//定义手机屏幕
type Screen01 struct {
Size float64 //屏幕尺寸
ResX, ResY int //屏幕分辨率 水平 垂直
}
//定义电池容量
type Battery struct {
Capacity string
}
//返回json数据
func getJsonData() []byte {
//tempData 接收匿名结构体(匿名结构体使得数据的结构更加灵活)
tempData := struct {
Screen01
Battery
HashTouchId bool // 是否有指纹识别
}{
Screen01: Screen01{Size: 12, ResX: 36, ResY: 36},
Battery: Battery{"6000毫安"},
HashTouchId: true,
}
jsonData, _ := json.Marshal(tempData) //将数据转换为json
return jsonData
}
4.2 命名嵌套
结构体命名嵌入是指结构体中的属性对应的类型也是结构体
给嵌入的结构体一个名字,让其成为另一个结构体的属性
适用于复合数据结构<嵌入匿名>
嵌套定义
type Book struct {
Author struct{
Name string
Aage int
}
Title struct{
Main string
Sub string
}
}
声明和初始化
b := &Book{
Author: struct {
Name string
Aage int
}{
Name: "xxxx",
Aage: 11,
},
Title: struct {
Main string
Sub string
}{
Main: "xxx",
Sub: "yyy",
},
}
//
b := new(Book)
b.Author.Aage = 11
b.Author.Name = "xxx"
嵌入命名,在外面定义
type Author struct {
Name string
Aage int
}
type Title struct {
Main string
Sub string
}
type Book struct {
Author Author
Title Title
}
示例
package main
import "fmt"
type Person struct {
Name string
Age int
}
type TeacherNew struct {
Pn Person
TeacherId int
}
func main() {
t2 := TeacherNew{
Pn: Person{
Name: "geek",
Age: 18,
},
TeacherId: 123,
}
fmt.Printf("[TeacherId: %v][Name: %v][Age: %v]", t2.TeacherId, t2.Pn.Name, t2.Pn.Age)
// [TeacherId: 123][Name: geek][Age: 18]
}
4.3 指针类型结构体嵌套
结构体嵌套(命名&匿名)类型也可以为结构体指针
声明&初始化&操作
type Book2 struct {
Author *Author
Title *Title
}
func (b *Book2) GetName() string {
return b.Author.GetName() + "book"
}
func TestMain8(t *testing.T) {
b1 := Book2{
Author: &Author{
Name: "ssgeek",
},
Title: &Title{},
}
b2 := &Book2{
Author: &Author{},
Title: &Title{},
}
}
使用属性为指针类型底层共享数据结构,当底层数据发生变化,所有引用都会发生影响
使用属性为值类型,则在复制时发生拷贝,两者不相互影响
4.4 结构体嵌套的实际意义
- 例如大项目对应复杂的配置文件,将公共的字段抽取出来,放到一个公共
common
的结构体 - cmdb、资产系统等类型设计
示例
package main
import "time"
// 云有云资源公共字段
type Common struct {
ChargingMod string // 付费模式:预付费和后付费
Region string // 区域
Az string // 可用区
CreateTime time.Time // 购买时间
}
type Ecs struct {
Common
guide string // 4C 16G
}
type Rds struct {
Common
dbType string // 代表数据库是哪一种
}
5、通过函数创建结构体对象
除了通过直接赋值创建结构体对象,还可以通过函数来创建,也就是把创建结构体对象的过程进行封装
即“工厂函数”
package main
import "fmt"
type Address struct {
Region string
Street string
No string
}
type User struct {
ID int
Name string
Addr *Address
}
func NewUser(id int, name string, region, street, no string) *User {
return &User{
ID: id,
Name: name,
Addr: &Address{region, street, no},
}
}
func main() {
me := User{
ID: 1,
Name: "geek",
Addr: &Address{"上海市", "南京路", "0001"},
}
me2 := me
me2.Name = "ss"
me2.Addr.Street = "黄河路"
fmt.Printf("%#v
", me.Addr)
fmt.Printf("%#v
", me2.Addr)
hh := NewUser(2, "hh", "北京市", "海淀路", "0001")
fmt.Printf("%#v
", hh)
}
6、结构体的可见性
结构体对外是否可见,在go
中受其首字母是否大写控制,结论是
结构体首字母大写则包外可见(公开的),否者仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否者仅包内可访问(内部的)
组合起来的可能情况:
- 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
- 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
- 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能
在包内使用 - 结构体名首字母小写,属性名小写:结构体只能在包内使用
- 结构体成员变量在同包内小写也是可以访问到的
总结:
- 跨包访问:全局变量、结构体本身、结构体成员变量、必须要首字母大写才可以暴露出来被访问到(在go中常见的是会给结构体绑定一个方法,返回小写的成员变量让外面访问到)
- 同包访问:上述变量首字母小写也可以被访问到
示例:
首先在tt
包下定义一个person
结构体,person
大写的时候外部的包可以访问到,person
小写的时候外部的包不可以访问到
package main
import (
"fmt"
"go-learning/chapter06/tt"
)
func main() {
p1 := tt.Person{
Name: "geek",
Age: 18,
}
fmt.Println(p1)
/*
# command-line-arguments
./last.go:9:8: cannot refer to unexported name tt.person
*/
}
See you ~