结构体
Go中没有"类"的概念,也不支持"类"的继承等面向对象的概念。
Go中通过结构体的内嵌,再配合接口,比面向对象具有更高的扩展性和灵活性。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或者部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,即struct。也就是我们可以通过struct来定义自己的类型。GO语言中通过struct来实现面向对象。
定义:
使用 type 和 struct 关键字来定义结构体。
type 类型名 struct { 字段名 字段类型 字段名 字段类型 ... } 类型名:标识自定义结构体的名称,在同一个包内不能重复 字段名:表示结构体字段名,结构体中的字段名必须唯一 字段类型:表示结构体字段的具体类型
type person struct { name string city string // name,city string // 相同类型的字段,可以写在一行 age int }
这样就拥有了一个person的自定义类型,它有name,city,age三个字段,表示三种属性。这样使用person结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字,年龄和居住城市等,本质上是一种聚合型的数据类型。
结构体实例化
只有当结构体实例化时,才会真正的分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
var 结构体实例 结构体类型
基本实例化
package main import ( "fmt" ) func main() { type person struct { name string city string age int } var p person p.name = "张三" p.city = "北京" p.age = 20 fmt.Println(p) // {张三 北京 20} fmt.Println(p.name) // 张三 }
可以通过 . 来访问结构体的字段,例如:p.name,p.age 等
匿名结构体:
在定义一些临时数据结构等场景下,还可以使用匿名结构体。
package main import ( "fmt" ) func main() { var user struct{Name string;Age int} user.Name = "张三" user.Age = 22 fmt.Println(user) // {张三 22} fmt.Printf("%#v \n",user) // struct { Name string; Age int }{Name:"张三", Age:22} }
创建指针类型结构体
可以通过使用new关键字对结构体进行实例化,得到的是 结构体的地址。
package main import "fmt" type person struct { name string city string age int8 } func main() { var p2 = new(person) fmt.Printf("%T \n",p2) // *main.person ,*代表指针,所以p2是 结构体指针 fmt.Printf("p2=%#v \n",p2) // p2=&main.person{name:"", city:"", age:0} fmt.Println(p2) // &{ 0} var p3 person fmt.Printf("%T \n",p3) // main.person fmt.Printf("p2=%#v \n",p3) // p2=main.person{name:"", city:"", age:0} fmt.Println(p3) // { 0} }
取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体进行一次new实例化操作。
package main import "fmt" type person struct { name string city string age int8 } func main() { var p3 = &person{} fmt.Printf("%T\n", p3) // *main.person fmt.Printf("p3=%#v\n", p3) // p3=&main.person{name:"", city:"", age:0} p3.name = "博客" fmt.Printf("p3=%#v\n", p3) // p3=&main.person{name:"博客", city:"", age:0} fmt.Println(p3.name) // 博客 }
结构体初始化
package main import "fmt" type person struct { name string city string age int8 } func main() { var p4 person fmt.Printf(" %#v \n ",p4) // main.person{name:"", city:"", age:0} fmt.Println(p4) // { 0} }
使用键值对初始化
package main import "fmt" type person struct { name string city string age int8 } func main() { var p5 = person{ city : "北京", name : "张三", } fmt.Println(p5) // 对结构体指针,进行 键值对初始化 var p6 = &person{ name : "李四", city : "上海", } fmt.Println(p6) }
初始化时,键对应结构体字段,值对应字段的初始值。
当某个字段没有指定初始值时,会使用该字段默认的零值。
赋值时,指定字段即可,不必按结构体字段照顺序进行赋值。
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候,不写键,直接写值。
package main import "fmt" type person struct { name string city string age int8 } func main() { var p8 = person{ "张三", "北京", 18, } fmt.Println(p8) // {张三 北京 18} }
简写时,必须初始化结构体的所有字段。
初始化值得顺序必须与结构体中字段的顺序一致。
该方式不能和键值初始化方式混用。
结构体内存布局
每个字段的内存地址独立,不一致
package main import "fmt" type test struct { a int8 b int } func main() { var t = test{ 1,1, } fmt.Printf("%p \n",&t.a) fmt.Printf("%p \n",&t.b) }
构造函数:用于新创建对象的初始化工作。
析构函数:用于在撤销对象前,完成一些清理工作。比如:释放内存
每当创建对象时,需要添加初始化代码时,则需要定义自己的构造函数;而对象撤销时,需要自己添加清理工作的代码时,则需要定义自己的析构函数。
Go语言的结构体没有构造函数,但是可以自己实现。下面的代码就实现了一个 person的构造函数。
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结果体指针类型
package main import "fmt" type person struct { name string city string age int8 } func newPerson(name,city string,age int8) *person{ return &person{ name : name, city : city, age : age, } } func main() { p9 := newPerson("张三","北京",18) fmt.Printf("%#v \n",p9) // &main.person{name:"张三", city:"北京", age:18} }
方法和接收者
GO语言中的方法,是一种作用于 特定类型变量 的函数,这种 特定类型变量 叫做 接收者,接收者的概念类似于其他语言的 this/self
func (接受者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
接收者变量:在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self/this,
接收者类型:接收者类型与参数类似,可以是 指针类型和非指针类型。
方法名,参数列表,返回参数:具体格式与函数定义相同。
package main import "fmt" type Person struct { name string age int8 } // 构造函数 func NewPerson(name string,age int8) *Person { return &Person{ name : name, age : age, } } // 方法 func (p Person) Dream() { fmt.Printf("%s的梦想是世界和平",p.name) } func main() { p1 := NewPerson("张三",18) p1.Dream() }
方法与函数得区别:函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
指针类型的接收者,由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者的任意成员变量,在方法结束后,修改都是有效的。
package main import "fmt" type Person struct { name string age int8 } // 构造函数 func NewPerson(name string,age int8) *Person { return &Person{ name : name, age : age, } } // 方法 func (p *Person) SetAge(newAge int8) { p.age = newAge // 修改 年龄 } func main() { p1 := NewPerson("张三",18) fmt.Println(p1.age) p1.SetAge(30) fmt.Println(p1.age) }
值类型的接收者
当方法作用于 值类型接收者时,GO语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
package main import "fmt" type Person struct { name string age int8 } // 构造函数 func NewPerson(name string,age int8) *Person { return &Person{ name : name, age : age, } } // 方法 func (p Person) SetAge2(newAge int8) { p.age = newAge // 修改 年龄 } func main() { p1 := NewPerson("张三",18) fmt.Println(p1.age) // 18 p1.SetAge2(30) fmt.Println(p1.age) // 18 }
什么时候应该使用指针类型接收者?
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的对象
- 保证一致性,如果某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
任意类型,添加方法
在GO语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
比如:我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
package main import "fmt" type Myint int func (m Myint) SayHello() { fmt.Println("hello world!!!") } func main() { var m1 Myint m1.SayHello() // hello world!!! m1 = 100 fmt.Printf("%#v \n",m1) // 100 fmt.Printf("%T \n",m1) // main.Myint }
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名,只有字段类型,这种没有字段名字的字段,称为 匿名字段
package main import "fmt" type Myint int type person struct { string int8 } func main() { p1 := person{ "张三", 18, } fmt.Printf("%#v \n",p1) // main.person{string:"张三", int8:18} fmt.Printf(p1.string) // 张三 }
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
package main import "fmt" type Address struct { Province string City string } type User struct { Name string Gender string Address Address } func main() { user1 := User{ Name : "张三", Gender : "男", Address : Address{ Province : "黑龙江", City : "哈尔滨", }, } fmt.Printf("%#v \n",user1) } // main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"黑龙江", City:"哈 尔滨"}}
嵌套匿名结构体
package main import "fmt" type Address struct { Province string City string } type User struct { Name string Gender string Address // 匿名结构体 } func main() { var user2 User user2.Name = "张三" user2.Gender = "男" user2.Address.Province = "黑龙江" // 通过 匿名结构体.字段 访问 user2.City = "哈尔滨" // 直接访问匿名结构体的字段名 fmt.Printf("%#v \n",user2) // main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"黑龙江", City:"哈 尔滨"}} }
当访问结构体成员时,会先在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名,这时 需要指定具体的内嵌结构体的字段。
package main import "fmt" //Address 地址结构体 type Address struct { Province string City string CreateTime string } //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 = "pprof" user3.Gender = "女" // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime user3.Email.CreateTime = "5000" //指定Email结构体中的CreateTime fmt.Printf("%#v\n", user3) } // main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"", City:"", CreateTime:"2000"}, Email:main.Email{Account:"", CreateTime:"5000"}}
结构体的 继承
Go语言使用的结构体也可以实现其他编程语言中 面向对象的继承
package main import "fmt" type Animal struct { name string } func (a *Animal) move() { fmt.Printf("%s 会动 \n",a.name) } type Dog struct { Feet int8 *Animal // 通过嵌套匿名结构体,实现继承 } func (d *Dog) wang() { fmt.Printf("%s 会叫 \n",d.name) } func main() { d1 := &Dog{ Feet : 4, Animal : &Animal{ // 嵌套结构体的指针 name : "乐乐", }, } d1.wang() d1.move() }
结构体字段的可见性
结构体中字段,大写开头表示可公开访问,小写表示仅在定义当前结构体的包中可访问
结构体与JSON序列化
package main import "fmt" import "encoding/json" //Student 学生 type Student struct { ID int Gender string Name string } //Class 班级 type Class struct { Title string Students []*Student } func main() { c := &Class{ Title: "101", Students: make([]*Student, 0, 200), } for i := 0; i < 10; i++ { stu := &Student{ Name: fmt.Sprintf("stu%02d", i), Gender: "男", ID: i, } c.Students = append(c.Students, stu) } //JSON序列化:结构体-->JSON格式的字符串 data, err := json.Marshal(c) if err != nil { fmt.Println("json marshal failed") return } fmt.Printf("json:%s\n", data) //JSON反序列化:JSON格式的字符串-->结构体 str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}` c1 := &Class{} err = json.Unmarshal([]byte(str), c1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Printf("%#v\n", c1) }
结构体标签 Tag
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体的后方定义,由一对反引号包裹起来,
结构体的标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。
为结构体编写Tag时,必须遵守键值对规则,结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。
package main import "fmt" import "encoding/json" //Student 学生 type Student struct { ID int `json:"id"` //通过指定tag实现json序列化该字段时的key Gender string //json序列化是默认使用字段名作为key name string //私有不能被json包访问 } func main() { s1 := Student{ ID: 1, Gender: "女", name: "pprof", } data, err := json.Marshal(s1) if err != nil { fmt.Println("json marshal failed!") return } fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"} }
删除map类型的结构体
package main import "fmt" type student struct { id int name string age int } func main() { ce := make(map[int]student) ce[1] = student{1, "xiaolizi", 22} ce[2] = student{2, "wang", 23} fmt.Println(ce) // map[1:{1 xiaolizi 22} 2:{2 wang 23}] delete(ce, 2) fmt.Println(ce) // map[1:{1 xiaolizi 22}] }
实现map有序输出
package main import ( "fmt" "sort" ) func main() { map1 := make(map[int]string, 5) map1[1] = "www.topgoer.com" map1[2] = "rpc.topgoer.com" map1[5] = "ceshi" map1[3] = "xiaohong" map1[4] = "xiaohuang" sli := []int{} for k, _ := range map1 { sli = append(sli, k) } sort.Ints(sli) for i := 0; i < len(map1); i++ { fmt.Println(map1[sli[i]]) } }