Go语言学习17-结构体(难度:五星级别 噩梦开始)

不少学GO的小伙伴们,都输在了这里。加油,相信自己可以成功!

0x00 Golang语言面向对象编程说明

1、Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言,所以我们应该说Golang支持面向对象编程特性

2、Golang没有类Class,Go语言的结构体(struct)和其它编程语言的类(class)有同等的低位,你可以理解Golang是基于struct来实现OOP特性的。

3、Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等

4、Golang仍然有面向对象编程的集成,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends关键字,继承是通过匿名字段来实现。

5、Golang可以面向对象,但是提倡面向接口编程。还有swift语言,都是这样的。

0x01 结构体的引入

之前的各种语言,类似于Java、php等,我们了解到万物均为对象,一个对象有很多的属性元素。那么在Go语言中,我们想要声明一个人,怎么办呢?下面这段代码是不是觉着特别特别麻烦啊。

func main (){
	//一位帅哥
	var name string = "shuaige"
	var age int = 31
	var gender string = "boy"

	//一位美女
	var name1 string = "meinv"
	var age int = 18
	var gender string = "girl"
}

缺点:不利于数据的管理、维护,一个人的属性属于一个对象,用变量管理实在是太分散了。

0x02 复习一下type关键字

自定义类型

在Go语言中有一些基本的数据类型,如string整型浮点型布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

我们之前见过的runebyte就是类型别名,他们的定义如下:

type byte = uint8
type rune = int32

那为什么不用int32呢?就是因为我们第一眼看int32会以为是一个数字,而rune就知道这是三个字节的字符。rune是内置的类型别名

类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
	var a NewInt
	var b MyInt
	
	fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
	fmt.Printf("type of b:%T\n", b) //type of b:int
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

0x03 结构体

上述的类型别名等,只能保存一个变量,而不是多个变量。所以需要一个能够存放多个变量值的类型。也就是struct

定义

使用typestruct关键字来定义结构体,具体代码格式如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

type person struct {
	name string
	city string
	age  int8
}

同样类型的字段也可以写在一行,

type person1 struct {
	name, city string
	age        int8
}

这样我们就拥有了一个person的自定义类型,它有namecityage三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

0x04 结构体初始化三种方式

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

第一种方式:基本实例化

type person struct {
	name,city string	//当属性类型一样时,可以写在一块
	age  int8
}

func main() {
	var p1 person
	p1.name = "沙河娜扎"
	p1.city = "北京"
	p1.age = 18
	fmt.Printf("p1=%v\n", p1)  //p1={沙河娜扎 北京 18}
	fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}

我们通过.来访问结构体的字段(成员变量),例如p1.namep1.age等。

第二种方式:key-value结构

type person struct {
	name string
	age  int64
}

//结构体初始化方式三:直接value
func main(){
    p2 := person{
       name : "第三种初始化方式",
       age : 20,
    }
    fmt.Printf("p2")
}

第三种方式:value结构

这种方式一定要注意,里面的值要与前面规定的对齐!

type person struct {
	name string
	age  int64
}

func main() {
	//结构体初始化方式二:key-value结构
	p3 := person{
	 	"第二种初始化方式",
		19,
	}
    fmt.Printf("p3为%v\n", p3)
}

三种方式对应的指针初始化方法

假设让你生成一个person类型指针,你怎么做?使用new函数会十分繁琐。

那么对应的初始化方式所对应指针的方式为:其实就是在person前面加了个&符号

var p3 = &person{
		name: "元帅",
		age:  18,
}

初始化3的指针方法:

p4 := &person{
	"小王子",
	19,
}

0x05 匿名结构体

多用于一些临时场景里面,只用这一次,就不怎么用了。而且多用于main函数里面,外面就是用type来声明结构体。

//匿名结构体
	var s struct {
		name string
		age  int
	}
	s.name = "beijing"
	s.age = 18
	fmt.Printf("type:=%T\nvalue:= %v", s.age, s.age)

直接使用var来声明一个结构体变量。

练习代码

type Person struct {
	Name  string
	Age   int
	Score int
	Hobby []string
}

//为什么要有结构体?一定是之前所学无法表示或者表示很繁琐新内容了,所以要开发一个新的东西,来解决这个痛点
//解决的就是无法定义一个多维度的东西
func main() {
	var a Person
	a.Name = "baizhantang"
	a.Hobby = []string{"足球", "篮球", "羽毛球"}
	a.Score = 98
	a.Age = 21
	fmt.Println(a)
	fmt.Println(a.Name)
	fmt.Println(a.Hobby)
	fmt.Println(a.Score)
	fmt.Println(a.Age)

	//匿名结构体
	var s struct {
		name string
		age  int
	}
	s.name = "beijing"
	s.age = 18
	fmt.Printf("type:=%T\nvalue:= %v", s.age, s.age)

}

0x06 结构体内存布局

结构体占用一块连续的内存。

type person struct {
	name   int8
	age    int8
	gender int8
}

func main() {
	var p1 person
	p1.name = 16
	p1.age = 18
	p1.gender = 15
	fmt.Printf("%p\n", &p1.name)
	fmt.Printf("%p\n", &p1.age)
	fmt.Printf("%p\n", &p1.gender)
	// fmt.Printf("%p\n",&p1.name)
}

image-20220222123016033

可以看到输出的结果就是连续的空间,一个个位数表示一个byte。如果是int64,8位8位的占。

image-20220222132051281

下面如果这个string类型,其实是进行了一个内存对齐的方式。

具体可以参考:

https://segmentfault.com/a/1190000017527311?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

image-20220222132152938image-20220219135054808

0x07 使用指针修改结构体某个属性

Go语言中,函数传参永远是拷贝,副本。底层原理也就是函数一旦被调用,就会创建一个函数栈帧,只不过这个栈帧会在函数结束的时候消失,也就是函数会被销毁。你修改的任何参数都不会发生变化。那我们如何去修改结构体的内容呢?参考函数的那节内存分析,我们得知,使用指针来对其进行操作,这就是指针在Go语言中存在的意义。

image-20220222104126180

type person struct {
	name, gender string
}

func changeGender(x *person) {
    //根据内存地址找到那个变量,修改的就是原来的变量
	//(*x).gender = "BBBBBBBBBBBBBBBBBBBBBBoy"	两种写法都可以,x默认指的就是*person,Go语言里面支持这样语法糖
    x.gender = "BBBBBBBBBBBBBBBBBBBBBBoy"	
}
func main() {
	var p person
	p.name = "holyshit"
	p.gender = "GGGGGGGGGGGGGGGGGirl"
	fmt.Println(p.gender)
	changeGender(&p)
	fmt.Println(p.gender)
	fmt.Println(p.name)
}

创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法糖。

0x08 构造函数

上面三种定义结构体的方法,实在是太复杂麻烦了,我不想弄了,怎么办?于是使用函数将这些代码封装起来。

返回一个结构体变量的函数。对比一下这两种构造函数的写法,左边的更为简洁,不过比较难以理解。

image-20220222151115118

以左边的代码为例子,需要考虑一个问题,构造函数的目的就是为了创造一个新的结构体,但是结构体是值类型。相当于每次调用这个函数,我都会拷贝来拷贝去,十分占用内存空间。消耗系统内存。

所以,构造函数返回的是结构体好,还是结构体指针比较好????

当结构体里面的变量不是很多,可以直接返回一个person结构体;

func newPerson()person{
	return person{
		name : name,
		age : age,
	}
}

当结构体内部字段比较多,比较大,是一个重量级别的,就可以返回一个指针。因为指针永远占用的是uint64类型,也就是8个字节,搬过来搬过去,不会消耗特别多的内存。

func newPerson()*person{	//这里返回的是一个指针
    return &person{
        name : name,
        age : age,
    }
}

image-20220222224308424

约定俗成,new开头的函数,一般都是构造函数,根据传的参数构造一个类型。

再举个例子:其实是很有规律的,敲多了就会了。

type game struct {
	name         string
	onlineplayer int
	comment      string
}

func newGame(name string, onlineplayer int, comment string) game {
	return game{
		name:         name,
		onlineplayer: onlineplayer,
		comment:      comment,
	}
}

func main() {
	game1 := newGame("CS GO", 3500000, "NEWBEE")
	fmt.Println("game1:", game1)
}

0x09 方法和接收者

什么是方法?

可以看到下面的这段代码中,函数wang()谁都可以去调用,所以就叫做函数。

type dog struct {
	name string
}

//构造函数
func newDog(name string) dog {
	return dog{
		name: name,
	}
}

//这个函数谁都可以调用,所以叫做函数
func wang() {
	fmt.Println("wwwwwww!")
}
func main() {
	d1 := newDog("fuckingbitch")
	fmt.Println(d1)
}

那么下面这段代码,就是有了所谓的对特定类来说的。

func (d dog) wang() {	//可以理解成,后面是d这个dog类型的变量的wang()方法,而wang方法内部调用了d的name属性。(d dog)表示接收者
	fmt.Println("wwwwwww!")
	fmt.Printf("%s:wangwangwang~", d.name)
}

总结一下下:方法是针对特定的类型调用,并且具有形参列表,返回值类型列表,还多了个接收者。

type dog struct {
	name string
}

//构造函数
func newDog(name string) dog {
	return dog{
		name: name,
	}
}
//这个函数谁都可以调用,所以叫做函数
//如果只能作用于某种特定的类型才能调用,这时候就叫做方法
//接收者默认使用类型首字母小写来表示,如dog的d。python用多的人可能会写成self
//php开发或者其他语言开发的可能会写成this

func (d dog) wang() { //(d dog)就是接收者
	fmt.Printf("%s:wangwangwang~", d.name)
}
func main() {
	d1 := newDog("fuckingbitch")
	d1.wang() //调用方法

}

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

//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()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

0x10 重点再讲解一下接收者

指针接收者:传地址进去

image-20220222214450794

值接收者:传拷贝进去

年龄没有发生任何变化,因为函数的任何传参都是值类型,相当于复制粘贴

type person struct {
	name string
	age  int
}

func newPerson(name string, age int) person {
	return person{
		name: name,
		age:  age,
	}
}
func (p person) sf() {
	p.age++
}
func main() {
	p1 := newPerson("shiqigege", 24)
	fmt.Println(p1.age)
	p1.sf()
	fmt.Println(p1.age)
}

image-20220222214016810

什么时候应该使用指针类型接收者

一般情况下我们确实采用

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象,指针只是uint64
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

image-20220222214714389

0x11 任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

嗨,看着挺复杂,其实看代码就知道了,简单。

这段代码肯定是错的,因为自定义类型加方法,不能使用go语言自身的关键词int。

func(int)hello(){	
	fmt.Println("我是一个int!")
}

那怎么办?直接重新定义给int换个名不得了

type myInt int

func (m myInt) hello() {
	fmt.Println("我是一个INT!")
}
func main() {
	m := myInt(100)
	m.hello()
}

posted @ 2022-02-24 18:52  sukusec不觉水流  阅读(29)  评论(0编辑  收藏  举报