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
我们之前见过的rune
和byte
就是类型别名,他们的定义如下:
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的类型是int
。MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了。
Go语言中通过struct
来实现面向对象。
0x03 结构体
上述的类型别名等,只能保存一个变量,而不是多个变量。所以需要一个能够存放多个变量值的类型。也就是struct
定义
使用type
和struct
关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
其中:
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person
(人)结构体,代码如下:
type person struct {
name string
city string
age int8
}
同样类型的字段也可以写在一行,
type person1 struct {
name, city string
age int8
}
这样我们就拥有了一个person
的自定义类型,它有name
、city
、age
三个字段,分别表示姓名、城市和年龄。这样我们使用这个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.name
和p1.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)
}
可以看到输出的结果就是连续的空间,一个个位数表示一个byte。如果是int64,8位8位的占。
下面如果这个string类型,其实是进行了一个内存对齐的方式。
具体可以参考:
0x07 使用指针修改结构体某个属性
Go语言中,函数传参永远是拷贝,副本。底层原理也就是函数一旦被调用,就会创建一个函数栈帧,只不过这个栈帧会在函数结束的时候消失,也就是函数会被销毁。你修改的任何参数都不会发生变化。那我们如何去修改结构体的内容呢?参考函数的那节内存分析,我们得知,使用指针来对其进行操作,这就是指针在Go语言中存在的意义。
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 构造函数
上面三种定义结构体的方法,实在是太复杂麻烦了,我不想弄了,怎么办?于是使用函数将这些代码封装起来。
返回一个结构体变量的函数。对比一下这两种构造函数的写法,左边的更为简洁,不过比较难以理解。
以左边的代码为例子,需要考虑一个问题,构造函数的目的就是为了创造一个新的结构体,但是结构体是值类型。相当于每次调用这个函数,我都会拷贝来拷贝去,十分占用内存空间。消耗系统内存。
所以,构造函数返回的是结构体好,还是结构体指针比较好????
当结构体里面的变量不是很多,可以直接返回一个person结构体;
func newPerson()person{
return person{
name : name,
age : age,
}
}
当结构体内部字段比较多,比较大,是一个重量级别的,就可以返回一个指针。因为指针永远占用的是uint64类型,也就是8个字节,搬过来搬过去,不会消耗特别多的内存。
func newPerson()*person{ //这里返回的是一个指针
return &person{
name : name,
age : age,
}
}
约定俗成,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 (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为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 重点再讲解一下接收者
指针接收者:传地址进去
值接收者:传拷贝进去
年龄没有发生任何变化,因为函数的任何传参都是值类型,相当于复制粘贴
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)
}
什么时候应该使用指针类型接收者
一般情况下我们确实采用
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象,指针只是uint64
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
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