Golang没有对象和继承的概念,也没有很多与面向对象相关的概念,例如继承、多态和重载。But,通过结构体的内嵌再配合接口,可以实现比面向对象更高的扩展性和灵活性。
0、初识
常见的数据类型数组、切片、字符串、字典、指针等都是指定某一类的数据类型,而结构体则是一种可以包含多种数据类型的数据类型。在Go语言特性下,若是要对标java、python面向对象,那么对应关系如下:
- 结构体(struct),对应面向对象的初始化字段信息,是一个包含多种类型的集合
- 方法(method),对应类的方法,一种作用于特定类型变量的函数,这种特定类型变量叫做接收者(Receiver),接收者的概念就类似于其他语言中的this或者 self
- 接口(interface),是一个或多个方法签名的集合,接口只有方法声明,没有实现,没有数据字段,可以实现多态等功能
1、结构体的定义
1.1 概述
结构体是复合类型(composite types),由一系列属性组成,每个属性都有自己的类型和值。
- 结构体也是值类型,因此可以通过 new 函数来创建
- 组成结构体类型的那些数据称为 字段(fields),每个字段都有一个类型和一个名字
- 在一个结构体中,字段名字必须是唯一的
- 结构体本质上是一种聚合型的数据类型
1.2 定义
定义结构体:首字母大写表示导包时可以对外访问,其中的元素也是一样
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
示例:同样类型的字段也可以写在一行
type struct1 struct {
x, y int
u float32
_ float32 // 填充
A *[]int
F func()
}
type person struct {
name, city string
age int8
}
2、结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
2.1 基本的实例化
.p1.Namep1.Age
func main() {
var p1 Person
p1.City = "大北京"
p1.Name = "alex"
p1.Age = 88
}
type Person struct{
Age int
Name, City string
}
2.2 匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体,即 无名结构体。
package main
import "fmt"
func main() {
var user struct{Name string; Age int} // 匿名结构体
user.Name = "alex"
user.Age = 18
fmt.Printf("%#v\n", user) // struct { Name string; Age int }{Name:"alex", Age:18}
}
2.3 结构体指针
常见数据类型:
- 值类型:int、float、bool、string、array、struct,值类型默认为深拷贝
- 引用类型:slice、map、function、pointer
2.3.1 创建指针类型的结构体
内置new()函数 创建引用类指针结构体,对结构体进行实例化,得到的是结构体的地址。
- make(T, args)只能用于内建类型map、slice、channel的内存分配。make(T, args),使用前必须初始化,返回一个非零值的T类型数据。即:make返回初始化后的(非零)值。
- new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。那么就是说,new()函数返回了一个指针,指向新分配的类型T的零值。即:new返回指针!
注意:
- new()初始化返回的数据 ,默认不是nil,而是空指针,指向了新分配的类型T的内存空间,里边的零值!
- 通过内置new()函数,创建结构体指针,实现浅拷贝
1)举例 :new() 返回结构体指针
func main() {
p1 := new(Person)
fmt.Printf("%T\n", p1) // *main.Person
fmt.Printf("%#v\n", p1) // &main.Person{Age:0, Name:"", City:""}
}
type Person struct{
Age int
Name, City string
}
p1
2)举例:new()实现的是结构体浅拷贝
package main
import "fmt"
func main(){
ptr := new(Person)
//(*ptr).Name = "alex"
ptr.Name = "alex" // 简写
ptr.Age = 88
fmt.Printf("%T,%v\n",ptr,*ptr)
// *main.Person,{alex 88}
ptr2 := ptr
ptr2.Name = "Pony"
fmt.Printf("%T,%v\n",ptr2,*ptr2)
// *main.Person,{Pony 88}
fmt.Printf("%T,%v\n",ptr,*ptr)
// *main.Person,{Pony 88}
}
type Person struct{
Name string
Age int
}
2.3.2 取结构体的地址实例化
&new
func main() {
p2 := &Person{}
fmt.Printf("%T\n", p2) // *main.Person
fmt.Printf("%#v\n", p2) // &main.Person{Age:0, Name:"", City:""}
(*p2).Name = "alex"
// go 语法糖 简写
p2.Age = 18
p2.City = "亦庄"
}
type Person struct{
Age int
Name, City string
}
3、结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值,而不是 nil
func main() {
p2 := new(Person)
fmt.Printf("%T\n", p2) // *main.Person
fmt.Printf("%#v\n", p2) // &main.Person{Age:0, Name:"", City:""}
fmt.Printf("%v", p2) // &{0 }
}
type Person struct{
Age int
Name, City string
}
3.1 四种方法初始化
package main
import "fmt"
func main(){
// 初始化结构体
// 1、方法一 结构体本质也是一种类型
var p1 Person
fmt.Println(p1) // { 0 },默认类型的零值
p1.Name = "王二狗子"
p1.Age = 30
p1.Sex = "男"
p1.Address = "北京"
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p1.Name,p1.Age,p1.Sex,p1.Address)
// 姓名:王二狗子,年龄:30,性别:男,地址:北京
// 2、方法二
p2 := Person{}
p2.Name = "Pony"
p2.Age = 28
p2.Sex = "男"
p2.Address = "南京"
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p2.Name,p2.Age,p2.Sex,p2.Address)
// 姓名:Pony,年龄:28,性别:男,地址:南京
// 3、方法三
p3 := Person{Name:"李小花", Age:25, Sex:"女", Address:"成都"}
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p3.Name,p3.Age,p3.Sex,p3.Address)
// 姓名:李小花,年龄:25,性别:女,地址:成都
p4 := Person{
Name: "alex",
Age: 88,
Sex: "男",
Address: "邯郸",
}
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p4.Name,p4.Age,p4.Sex,p4.Address)
// 姓名:alex,年龄:88,性别:男,地址:邯郸
// 方法四:注意位置参数,务必位置对齐
p5 := Person{"eva", 26, "女", "海淀"}
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p5.Name,p5.Age,p5.Sex,p5.Address)
// 姓名:eva,年龄:26,性别:女,地址:海淀
}
// 1、定义结构体
type Person struct {
Name string
Age int
Sex string
Address string
}
4、结构体的特性
4.1 结构体的内存布局
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。
type test struct {
a,b,c,d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
输出:
// 结构体占用一块连续的内存
n.a 0xc00000a098
n.b 0xc00000a099
n.c 0xc00000a09a
n.d 0xc00000a09b
4.1 空结构体
空结构体Person02如下所示,空结构体不占内存空间:
func main() {
var p5 Person
var p6 Person02
fmt.Printf("%v\n", unsafe.Sizeof(p5)) // 40
fmt.Printf("%v\n", unsafe.Sizeof(p6)) // 0 空结构体不占内存空间
}
type Person struct{
Age int
Name, City string
}
// 空结构体 Person02
type Person02 struct{}
5、构造函数
Go语言的结构体没有构造函数,但也可以自己实现之。值得留意的是,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以推荐该构造函数返回的是结构体指针类型。
package main
import "fmt"
func main(){
// 调用构造函数 NewPerson
p8 := NewPerson("ali","hangzhou", 18)
fmt.Printf("%#v\n", p8) // &main.person{name:"ali", city:"hangzhou", age:18}
// 常规初始化
p9 := new(person)
p9.name="ali"
p9.city="hang"
p9.age = 19
fmt.Printf("%#v\n", p9) // &main.person{name:"ali", city:"hang", age:19}
}
type person struct{
name,city string
age int8
}
func NewPerson(name, city string, age int8) *person{
return &person{
name: name,
city: city,
age: age,
}
}
6、结构体的匿名字段
结构体里边允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
- 匿名字段默认采用类型名作为字段名
- 结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
func main(){
p1 := Person{
"alex",
18,
}
fmt.Printf("%#v\n", p1) // main.Person{string:"alex", int:18}
fmt.Println(p1.string, p1.int) // alex 18
}
type Person struct{
// 一个结构体中同种类型的匿名字段只能有一个
string
int
}
7、嵌套结构体
7.1 结构体的“继承” 之 “属性继承”
Go语言结构体中这种含匿名字段和嵌套结构体的结构,可近似地理解为面向对象语言中的继承概念。
- 嵌套结构体的字段,必须通过嵌套结构体.字段名访问
- 匿名嵌套结构体的字段,可以直接用匿名结构体.字段名访问
一个结构体中可以嵌套包含另一个结构体或结构体指针:
func main(){
p1 := new(Person)
(*p1).Name = "alex"
(*p1).Age = 18
(*p1).Address.City = "beijing" // 通过嵌套结构体.字段名访问
(*p1).Address.Province = "beijing" // 通过嵌套结构体.字段名访问
fmt.Printf("%#v\n", p1)
// &main.Person{Name:"alex", Age:18, Address:main.Address{Province:"beijing", City:"beijing"}}
}
type Person struct{
Name string
Age int8
Address Address // 嵌套结构体
}
type Address struct{
Province string
City string
}
7.2 嵌套匿名字段
Address
func main(){
p1 := new(Person)
(*p1).Name = "alex"
(*p1).Age = 18
(*p1).City = "beijing" // 直接访问匿名结构体的字段名
(*p1).Address.Province = "beijing" // 通过匿名结构体.字段名访问
fmt.Printf("%#v\n", p1)
// &main.Person{Name:"alex", Age:18, Address:main.Address{Province:"beijing", City:"beijing"}}
}
type Person struct{
Name string
Age int8
Address // 匿名字段,嵌套结构体
}
type Address struct{
Province string
City string
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
7.3 嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义必须通过指定具体的内嵌结构体字段名。
func main(){
var p8 Person
p8.Name = "barry"
p8.Age = 26
//p8.CreateTime = "2019" // ambiguous selector p8.CreateTime,产生字段歧义
p8.Address.CreateTime = "2021" // 指定Address结构体中的CreateTime
p8.Email.CreateTime = "2021" // 指定Email结构体中的CreateTime
}
// Address 地址结构体
type Address struct{
Province string
City string
CreateTime string
}
// Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
// Person 结构体
type Person struct{
Name string
Age int
Address // 匿名字段,嵌套结构体
Email // 匿名字段,嵌套结构体
}
10、结构体字段的可见性
结构体中:字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
11、结构体与JSON序列化
"":,
- JSON序列化:结构体–>JSON格式的字符串
- JSON反序列化:JSON格式的字符串–>结构体
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
完整示例如下:
package main
import (
"encoding/json"
"fmt"
)
func main(){
// 初始化 c
c := &Class{
Title:"200",
Students: make([]*Student, 0, 10), // 初始长度为0,容量为10
}
for i:=0;i<1;i++{
stu := &Student{
Name: "alex",
ID: i+1,
Gender: "男",
}
c.Students = append(c.Students, stu)
}
// JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c) // json 序列化,返回 json 和 err
if err != nil{
fmt.Println("JSON序列化失败~")
return
}
fmt.Printf("json: %s\n", data) // 这里注意 %s 的用法!!
// json: {"Title":"200","Students":[{"Name":"alex","ID":1,"Gender":"男"}]}
// JSON反序列化:JSON格式的字符串-->结构体
str1 := `{"Title":"200","Students":[{"Name":"alex","ID":1,"Gender":"男"}]}` // 注意 `` 符合
c1 := &Class{} // 初始化声明c1类型
err = json.Unmarshal([]byte(str1), c1) // json 反序列化,返回 err
if err != nil{
fmt.Println("反序列化失败~")
return
}
fmt.Printf("%#v\n", c1)
// &main.Class{Title:"200", Students:[]*main.Student{(*main.Student)(0xc00007c750)}}
}
type Student struct{
Name string
ID int
Gender string
}
type Class struct {
Title string
Students []*Student // 创建字段 Students,其值对应Student指针的切片
}
12、结构体标签Tag
TagTag
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
Tag
package main
import (
"encoding/json"
"fmt"
)
func main(){
stu := Student{
ID: 1,
Gender: "男",
name: "alex",
}
data,err := json.Marshal(stu)
if err != nil{
fmt.Print("序列化失败哦~")
return
}
fmt.Printf("json字符串:%s", data)
// json字符串:{"id":1,"Gender":"男"}
// 可以看出:
// 1) 可以指定标签,实现json序列化该字段时的key
// 2)私有字段不能被Json访问
}
type Student struct{
ID int `json:"id"` // 通过指定标签tag,实现json序列化该字段时的key就是id,而非ID
Gender string //json序列化是默认使用字段名作为key
name string // 字段首字母小写,私有字段不能被json包访问
}
13、方法与接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
- Golang 的方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
- 只能为当前包内命名类型定义方法。
- 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
- 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
- 不支持方法重载,receiver 只是参数签名的组成部分。
- 可用实例 value 或 pointer 调用全部方法,编译器自动转换
- 一个类型加上它的方法等价于面向对象中的一个类。它们必须在同一个包下。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
13.1 方法的定义
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表)(返回值列表){
函数体
}
// 参数和返回值可以省略
selfthisPersonpConnectorc
package main
type Test struct{}
// 无参数、无返回值
func (t Test) method0() {
}
// 单参数、无返回值
func (t Test) method1(i int) {
}
// 多参数、无返回值
func (t Test) method2(x, y int) {
}
// 无参数、单返回值
func (t Test) method3() (i int) {
return
}
// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
return
}
// 无参数、无返回值
func (t *Test) method5() {
}
// 单参数、无返回值
func (t *Test) method6(i int) {
}
// 多参数、无返回值
func (t *Test) method7(x, y int) {
}
// 无参数、单返回值
func (t *Test) method8() (i int) {
return
}
// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
return
}
func main() {}
13.2 方法的案例
func main(){
dog := Dog{
Name: "alex",
Age: 2,
}
// 调用 方法执行,类似于类的方法调用
dog.Eat()
}
type Dog struct{
Name string
Age int
}
func (d Dog) Eat(){
fmt.Printf("%v汪汪~~",d.Name) // alex汪汪~~
}
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("小王子", 25)
p1.Dream() // 小王子的梦想是学好Go语言!
}
13.3 指针类型的接收者
指针类型的接收者由一个结构体的指针组成。
thisself
StudentSetScore
func main(){
stu := Student{
Name: "alex",
Score: 66,
}
fmt.Printf("%v\n", stu.Score) // 66
stu.SetScore(88)
fmt.Printf("%v\n", stu.Score) // 88
}
type Student struct{
Name string
Score int
}
// 使用指针接收者
func (s *Student) SetScore (NewScore int) {
//(*s).Score = NewScore // 根据内存地址,修改原地址所存储的数据
s.Score = NewScore // 简写
所以,日常coding中 指针类型的接收者 较为常见。
13.2 值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
func main(){
stu := Student{
Name: "alex",
Score: 66,
}
fmt.Printf("%v\n", stu.Score) // 66
stu.SetScore(88)
fmt.Printf("%v\n", stu.Score) // 66,可见,值传递并不会修改原地址的值,进行的是深拷贝
}
type Student struct{
Name string
Score int
}
func (s Student) SetScore (NewScore int){
s.Score = NewScore // 值 传递,这里么有简写,本来就是值
}
日常coding中,很少用值传递,毕竟值传递的方法操作了个寂寞~ 原地址数据并未修改
13.3 指针类型接收者的使用场景
- 需要修改接收者中的值
- 不需修改,但接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
13.4、方法的拓展:任意类型添加方法
int
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 200
fmt.Printf("%#v %T\n", m1, m1) //200 main.MyInt
}
不能给别的包的类型定义方法
14、结构体的“继承” 之 “方法继承”
在Golang中使用结构体和方法,也可以实现其他编程语言中面向对象的继承。
func main(){
dog := &Dog{
Feet: 4,
Animal: &Animal{ // 注意!!嵌套的是指针 !!
Name: "alex",
Age: 3,
},
}
dog.Sleep() // alex is sleeping
dog.Run() // alex is running
}
// Animal 结构体
type Animal struct{
Name string
Age int
}
// Animal类型 的 Sleep方法
func (a *Animal) Sleep(){
fmt.Printf("%v is sleeping\n", a.Name)
}
// Dog 结构体
type Dog struct{
Feet int
*Animal // 匿名嵌套结构体指针!!,类似于继承
}
// Dog类型 的 Run方法
func (d *Dog) Run(){
fmt.Printf("%v is running\n", d.Name)
}
15、结构体和方法补充点
slice和map这两种数据类型都包含了指向底层数据的指针,那么在复制的过程中都是浅拷贝,在传递指针地址的时候,尤为注意数据的安全性。
func main(){
person := Person{
Name: "alex",
Age: 0,
dreams: nil,
}
fmt.Printf("%v\n",person.dreams)
data := []string{"吃饭","睡觉","打豆豆"}
person.SetDreams(data)
fmt.Printf("%v\n",person.dreams) // [吃饭 睡觉 打豆豆]
// 对 切片data操作,竟然也会影响到了 person数据!!
data[0] = "LOL"
fmt.Printf("%v\n",person.dreams) // [LOL 睡觉 打豆豆]
// 原因是引用类型数据,本身就是浅拷贝,所以在复制的时候,需要拷贝引用类型的数据,而不是直接拷贝地址
}
type Person struct{
Name string
Age int
dreams []string
}
func (p *Person) SetDreams(NewDreams []string){
p.dreams = NewDreams
}
原因是引用类型数据,本身就是浅拷贝,所以在复制的时候,需要拷贝引用类型的数据,而不是直接拷贝地址
package main
import "fmt"
func main(){
person := Person{
Name: "eva",
Age: 0,
Dreams: nil,
}
fmt.Printf("%v\n",person.Dreams) // []
data := []string{"恰饭","睡觉","打豆豆"}
person.SetDreams(data)
fmt.Printf("%v\n",person.Dreams) // [恰饭 睡觉 打豆豆]
// 修改切片 data 数据
data[0] = "王者荣耀"
fmt.Printf("%v\n", person.Dreams) // [恰饭 睡觉 打豆豆]
// 由此可以,对于引用类型数据的指针接收者,务必copy其值,重新分配内存空间,保障数据安全
}
type Person struct{
Name string
Age int
Dreams []string
}
func (p *Person) SetDreams(NewDreams []string){
p.Dreams = make([]string, len(NewDreams)) // 初始化
copy(p.Dreams, NewDreams) // 注意这里是拷贝值,重新分配内存地址给p.Dreams
}
注意:由此可以,对于引用类型数据slice和map的指针接收者,务必copy其值,重新分配内存空间,保障数据安全
16、练习
16.1 思考执行逻辑
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stu := []student{
{name: "小王子", age: 18},
{name: "娜扎", age: 23},
{name: "雪儿", age: 9000},
}
fmt.Println(stu) // [{小王子 18} {娜扎 23} {雪儿 9000}]
for _, stu := range stu {
m[stu.name] = &stu
}
fmt.Println(m) // map[娜扎:0xc0000040d8 小王子:0xc0000040d8 雪儿:0xc0000040d8]
for k, v := range m {
fmt.Println(k, "=>", v.name) // 注意这里是指针取值,简写了
//fmt.Println(k, "=>", (*v).name) // 注意这里是指针取值,简写了
}
}