前言
Go 语言中,没有类的概念,使用结构实现类似类的功能。
语言内置的基础数据类型是用来描述一个值,而结构体是用来描述一组值。
数组、切片和 Map 可以用来表示 同一种数据类型 的集合,但是当我们要表示 不同数据类型 的集合时就需要用到结构体。
定义结构体
使用 type
和 struct
关键字来定义结构体
type 结构体名 struct {
字段名 字段类型
字段名 字段类型
…
}
// 例如
type person struct {
name string
city string
age int8
}
结构体名:表示结构体的名称,在同一个包内不能重复,并且最好不要以包的名称开头,不然 idea 会警告
字段名:表示结构体字段名,结构体中的字段名必须唯一
字段类型:表示结构体字段的具体类型
通常结构体中一个字段占一行,但是类型相同的字段,也可以放在同一行,同样类型的字段也可以写在一行
type person struct {
name, city string // name, city是同一个类型
age int8
}
一个结构体中的字段名是唯一的,例如一下代码,出现了两个 Name 字段,是错误的:
type Student struct {
Name string
Name string
}
结构体还可以省略字段名称,这种字段叫:结构体匿名字段。匿名结构体字段,默认会采用类型名作为字段名;由于结构体要求字段名称必须唯一,所以结构体中同种类型的匿名字段只能有一个,不然就会重复。
type person struct {
string // 匿名字段
int8 // 匿名字段
}
func main() {
p1 := person{
string: "张三",
int8: 18,
}
fmt.Println(p1.string)
fmt.Println(p1.int8)
}
结构体还可以定义字段的可见性,字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问),那么其他 package 就无法直接使用该字段
// 在包 pk1 中定义 Student 结构体
package pk1
type Student struct{
Age int
name string
}
// 在另外一个包 pk2 中调用 Student 结构体
package pk2
func main(){
stu := Student{}
stu.Age = 18 //正确
stu.name = "name" // 错误,因为`name` 字段为小写字母开头,不对外暴露
}
对结构体 json 化的时候,私有的字段将会丢失
type Student struct {
Id int
Name string
age int // 私有
}
func main() {
studentInfo := Student{
Id: 1,
Name: "小明",
age: 35,
}
// json 格式化
data, err := json.Marshal(studentInfo)
if err != nil {
fmt.Println("json encode 错误")
return
}
fmt.Printf("%s", data) // {"Id":1,"Name":"小明"},年龄字段已经丢失了
}
这样对结构体进行 json 的时候,默认输出字段的名称就是结构体字段的名称,我们经常需要修改,不希望输出的字段名称是大写字母开头的,这个时候就需要 结构体标签 的功能了,结构体标签可以是多个,用空格隔开
type Student struct {
Id int `json:"id"`
Name string `json:"name"`
age int // 私有
}
func main() {
studentInfo := Student{
Id: 1,
Name: "小明",
age: 35,
}
// json 格式化
data, err := json.Marshal(studentInfo)
if err != nil {
fmt.Println("json encode 错误")
return
}
fmt.Printf("%s", data) // {"id":1,"name":"小明"},这个时候字段的名称就是小写的了
}
结构体初始化
声明结构体的类型的变量的时候进行初始化,这个时候结构体的变量每个字段都有它默认类型的零值,这个和切片、map 不一样,切片和 map 定义之后,需要初始化才能使用
type person struct {
name string
city string
age int8
}
func main() {
var p person
fmt.Printf("%#v\n", p) // 输出main.person{name:"", city:"", age:0}
}
结构体是通过 {}
括号进行初始化的
func main() {
p1 := person{}
fmt.Printf("%#v\n", p1) // 输出main.person{name:"", city:"", age:0}
p2 := person{
name: "张三",
city: "厦门",
age: 30, //最后一个逗号不能忘记
}
fmt.Printf("%#v\n", p2) // 输出main.person{name:"张三", city:"厦门", age:30}
}
可以对结构体的部分字段进行初始化,其它字段的值是默认类型的零值
type person struct {
name, city string
age int8
}
func main () {
p:= &person{
name:"张三",
age: 30,
}
fmt.Printf("%#v",p)
}
还可以使用值列表对结构体进行初始化,不需要写 key,不过有几个约束条件:
- 必须初始化结构体的 所有字段
- 初始值的填充顺序必须与字段在结构体中的声明 顺序一致
- 该方式不能和键值初始化方式 混用
type person struct {
name, city string
age int8
}
func main () {
p := person{
"张三",
"厦门",
30,
}
fmt.Printf("%#v",p)
}
结构体变量,可以通过 .
访问并且设置结构体字段的值
type person struct {
name, city string
age int8
}
func main () {
var p person // 声明结构体的类型的变量
// 通过`.`访问并且设置结构体字段的值
p.name = "张三"
p.city = "厦门"
p.age = 30
fmt.Printf("p=%#v\n",p)
fmt.Println(p.name)
}
点号不仅可以作用在结构体变量上,还可以作用在结构体指针上
type person struct {
name, city string
age int8
}
func main() {
var p *person = &person{}
(*p).name = "张三"
(*p).city = "厦门"
(*p).age = 30
fmt.Printf("p=%#v\n", (*p))
fmt.Println((*p).name)
}
还可以获取成员变量的指针,通过指针来访问成员变量
type person struct {
name, city string
age int8
}
func main() {
var p person = person{}
name := &p.name
fmt.Println(*name)
}
匿名结构体
正常情况下都是先定义结构体,然后把变量声明为结构体的,但是匿名结构体主要用于临时使用,不需要先定义结构体的类型,可以直接把变量声明为没有名称的结构体
func main () {
// 不需要type定义结构体
var user struct{
name string
married bool
}
user.name = "张三"
user.married = false
fmt.Println(user)
}
指针类型的结构体
使用 new 关键字对结构体实例化,可以得到结构体的指针
type person struct {
name, city string
age int
}
func main() {
var p = new(person)
fmt.Printf("%#v\n", p) // 输出&main.person{name:"", city:"", age:0}
}
还有一种方式也可以快速的获取结构体的指针,那就是 &
符号,person{}
表示对 person 结构体进行初始化,然后用 &
符号进行取值
func main() {
p := &person{}
fmt.Printf("%#v\n", p) // 输出&main.person{name:"", city:"", age:0}
}
然后可以同 *
号来访问指针地址的实际位置
var p = new(person)
(*p).name = "张三"
(*p).city = "厦门"
(*p).age = 30
fmt.Printf("%#v\n", p) // &main.person{name:"张三", city:"厦门", age:30}
正常情况下,要改变指针的实际值,都需要用 *
号来指向实际值,然后修改,但是每次都这样就显得麻烦,所以在结构体中,可以省略这步骤,直接访问,这个是语法糖的形式,如下所示也是可以的
var p = new(person)
p.name = "张三"
p.city = "厦门"
p.age = 30
fmt.Printf("%#v\n", p)
结构体内存布局
结构体变量占用一块连续的内存,也就是说字段在内存中是连续的,但是空的匿名结构体是不占用内存空间的
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
结构体构造函数
结构体没有构造函数,但是可以自己实现,其实就是构造一个结构体类型变量,并且赋值,并且函数名称约定成俗的以 new 开头。结构体是值类型,赋值的话是拷贝,所以复杂的结构体赋值会影响性能,因此构造函数最好返回指针类型
type person struct {
name, city string
age int
}
func newPerson(name string, city string, age int) *person {
return &person{
name: name,
city: city,
age: age,
}
}
func main() {
p := newPerson("小明", "厦门", 18)
fmt.Printf("%#v\n", p) // &main.person{name:"小明", city:"厦门", age:18}
}
方法
Go 语言中,接收者的类型只能为用关键字 type 定义的类型,例如自定义类型,结构体。类型的方法指明该方法属于某个类型,只有这个类型的实例才能调用。同一个接收者的方法名不能重复。值作为接收者无法修改其值,如果有更改需求,需要使用指针类型。
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
同样,对结构体这种自定义类型,也可以添加自己的方法函数
type person struct {
name, city string
age int
}
func (p person) dream() {
fmt.Printf("%s的梦想是赚大钱", p.name)
}
func main() {
p := person{
name: "小明",
city: "厦门",
age: 18,
}
p.dream()
}
关于接受者变量和类型有几个要注意的是:
- 接受者变量的名称,一般使用类型的首个小写字母,例如上例中 MyInt 类型的接受变量就用 m
- 接受者变量的类型有值类型和指针类型,但是并不要求调用者也要对于的值类型或者指针类型,调用者只要是跟接受者同一个类型就好了,编译器会自动转换;值类型的接受者是对实例的拷贝,方法内部的改变不会影响到调用者实例本身;指针类型的接收者,方法内部对实例的改变,将会影响到实例本身的值。
- 结构体方法最后统一使用值接受者,或者指针接受者,一般统一使用指针接受者,保持统一,不然 idea 会警告
如果构造函数返回的是接口类型,并且接口实例的方法是指针接受者,那么构造必须返回指针,不然会报错
type IServer interface {
Start()
}
type Server struct {
Name string
IPVersion string
IP string
Port int
}
// 指针类型接受者
func (s *Server) Start() {
}
// 返回接口类型
func NewServer(name string) IServer {
// !!因为接受者是指针类型,所以必须返回指针
s := &Server{
Name: name,
IPVersion: "tcp4",
IP: "0.0.0.0",
Port: 8999,
}
return s
}
往值上添加方法,接受者就是值本身
type myInt int
func (m myInt) PrintValue() {
fmt.Println(m)
}
func main() {
var m myInt
m = 10
m.PrintValue() // 10
}
结构体的嵌套
一个结构体中可以包含另一个结构体或结构体指针,就好比 json 对象中,某个字段又是一个 json 对象
type Address struct {
Province string
City string
}
type User struct {
Name string
Gender string
Address Address // 嵌套另外一个结构体
}
func main() {
userInfo := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf("%#v\n", userInfo) // main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
但是需要注意的是,如果嵌入的结构体是本身,那么只能用指针。请看以下例子。
type Tree struct {
value int
left, right *Tree
}
func main() {
tree := Tree{
value: 1,
left: &Tree{
value: 1,
left: nil,
right: nil,
},
right: &Tree{
value: 2,
left: nil,
right: nil,
},
}
fmt.Printf(">>> %#v\n", tree)
}
如果嵌套结构体的字段是匿名字段,那么访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找,这个特性很重要,我们就可以实现类似类里的属性和方法的覆盖特性。
type Address struct {
Province string
City string
}
type User struct {
Name string
Gender string
Address
}
func main() {
userInfo := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Println(userInfo.City) // 输出了匿名字段Address结构体上的字段了
}
如果嵌套的结构体数量一多,很可能字段会重复,如果利用嵌套匿名字段结构体的查找规则,很可能会出问题,所以如果嵌套比较多结构体,最好指定具体的内嵌结构体字段名会比较清晰
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string // 和email结构体中的createTime重复
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime,会报错
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}
利用结构体的嵌套特性,我们就可以实现结构体的继承了。
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
func (a *Animal) eat() {
fmt.Printf("%s会吃!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
Animal //通过嵌套匿名结构体实现继承
}
// 重写父类的方法
func (d *Dog) eat() {
fmt.Printf("%s会大口的吃~\n", d.name)
}
// 子类新定义的方法
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
dogInfo := &Dog{
Feet: 4,
Animal: Animal{
name: "旺财",
},
}
dogInfo.eat()
dogInfo.wang() //旺财会汪汪汪~
dogInfo.move() //旺财会动!
}
结构体的比较
前提是结构体中的字段类型是可以比较的,应该是各个字段之间进行比较,如果每个字段的值都相等的话,就是相等
type Tree struct {
value int
left, right *Tree
}
func main() {
tree1 := Tree{
value: 2,
}
tree2 := Tree{
value: 1,
}
fmt.Printf(">>> %#v\n", tree1 == tree2)
}
评论 (0)