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)   // 注意这里是指针取值,简写了
	}
}