ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-02-24 19:00:31  阅读:159  来源: 互联网

标签:name 17 age 五星级 person 类型 Go type fmt


不少学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()
}

标签:name,17,age,五星级,person,类型,Go,type,fmt
来源: https://www.cnblogs.com/sukusec301/p/15933154.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有