面向对象编程(OOP):具体概念不讲了,学过java,py都知道,golang中没有类的概念,可以用结构体来模拟替代类。
Golang支持面向对象编程的特性,去掉了传统OOP语言的集成,方法重载,构造函数和析构函数,隐藏的this指针等。
结构体:
抽取事物相同特征和行为形成新的数据类型,就是一个结构体,通过结构体可以创建很多实例(类似py就是类创建多个实例);
上述是为了解决描述一个事物拥有的不同属性和方法在Golang中无法统一定义,例如年龄不应该为string型,可是为了int又需要额外定义一个数组或者其他方式保留,太蛋疼。
结构体定义:
★★★值类型,当声明一个结构体type定义时就已经分配了内存,什么都不传就是 {0和空等}
★★★<struct_name>首字母大写其他程序可以import这个包,结构体变量首字母大写其他程序可以import这个包使用这个变量
type <struct_name>(nbaPlayers) struct { // type,struct 关键字,结构体名字自己起,和python中的 Class <class_name> 同理
Name string
Age int
color string
hobbie string
}
//上述定义完成后,下方即可使用结构体作为变量
var player1 nbaPlayers //定义完了,nbaPlayers就是个结构体,和int,string等一样
示例:
package main
import "fmt"
func main() {
type nbaPlayers struct {
Name string
Age int
color string
hobbie string
}
//定义方式1:
var player1 = nbaPlayers{"阿狗",23,"red","eatting"}
fmt.Println(player1) // {阿狗 23 red eatting}
//定义方式2: 使用 <实例>.<属性> 来读取相应的数值
var player2 nbaPlayers
player2.Name = "阿猫"
player2.Age = 20
player2.color = "blue"
player2.hobbie = "sleeping"
fmt.Println(player2) // {阿猫 20 blue sleeping}
//具体打印某个属性
fmt.Println(player2.Age) // 20
//具体还可以绑定nbaPlayers的方法(函数),后续再说
}
字段/属性/Fields
由于是值类型,实例产生后不赋值,则各种数据类型都为默认值:
值类型:
bool:false , string:"" , int::0 , [n]int:[ n个0 ]
引用类型:
ptr , slice , map 的零值都是nil,说明没有分配内存空间,需要make()后才可以使用
不同结构体变量的字段是独立互不影响的,因为是值类型,改变一个实例的一个字段不会影响到另一个实例。
package main
import "fmt"
func main() {
type nbaPlayers struct {
Name string
Age int
color string
hobbie string
Ptr *int
Slice []int
Map1 map[string]string
}
/*
默认下面三种引用类型都是nil的,可以使用if ptr == nil 成立则输出结果来判断
*/
var temp = 2
var player1 nbaPlayers
player1.Ptr = &temp
fmt.Println(player1.Ptr)
if player1.Slice == nil {
fmt.Println("OK")
}
player1.Slice = make([]int,2) //
player1.Slice[0] = 10
player1.Slice[1] = 20
for i := 0 ; i < 5 ; i++ {
player1.Slice = append(player1.Slice, i)
}
fmt.Println(player1.Slice)
}
结构体赋值的四种方式:
package main
import "fmt"
func main() {
type Pl struct {
Name string
Age int
}
//方式1
var p1 Pl = Pl{"dd",20,}
fmt.Println(p1)
//方式2
var p2 Pl
p2.Name = "ee"
p2.Age = 20
fmt.Println(p2)
//方式3:返回结构体指针
var p3 *Pl = new(Pl)
p3.Name = "ff" //设计者简化了写法,实际上p3都已经默认加了 (*p3)
(*p3).Age = 20
fmt.Println(*p3)
//方式4:返回结构指针2
var p4 *Pl = &Pl{}
p4.Name = "gg" //设计者简化了写法,实际上p4都已经默认加了 (*p3)
(*p4).Age = 20
fmt.Println(*p4)
}
结构体内存分配机制
- 两个结构体实例:p1,p2, p2 = p1,修改p2的值,不会影响p1,符合值拷贝,不再过多赘述。
- 如果 p2是指针类型结构体,那么 p2 = &p1,则修改p2中的结构体字段值,会影响p1,符合之前引用传递,大同小异。
- 结构体所有字段在内存中是连续分布的,可以通过地址加减来获得不同的字段对应的值。
- 嵌套型结构体,外层结构体如果是指针类型,那么外层结构体指针的地址一定是连续的,但是指向的内层结构体的值不一定连续,这个需要根据计算机实际情况来算。
结构体强转
- 可以转换,但是要求是 两个结构体的字段和类型要完全相同,名字个数类型都必须一致!
- 定义一个结构体stuc1, var stuc2 stuc1,这样是不可以会报错,也需要 stuc2 = Stu(stuc1), 因为重新type之后,stuc1和stuc2不是同一个数据类型 同理应用到普通变量赋值:
var i integer = 10
var j int = 20
j = i //也会报错!必须强转 j = int(i)这样才可以,虽然i也是int但是它是integer...
结构体使用细节和原理
- 一个Go语言写的应用,客户端进行访问,Go语言返回客户端的需求信息,一般将结构体序列化成json格式(字符串)返回给服务器。
- 序列化需要 import “encoding/json”,注意事项详见下面代码,今后开发会遇到很多 `json:"<字段名称>"` 的方式。
- json.Marshal(<实例>) 会经常用到。
package main import ( "encoding/json" "fmt" ) type Nbaplayer struct { Name string `json:"name"` //序列化需要引入import "encoding/json" Num int `json:"num"` //由于json函数中使用结构体参数表示Nbaplayer要在别的包被使用,所以必须变量大写,不然返回空 Abil string `json:"abil"` //使用`json:"<字段名称>"` 这样后续json字符串首字母变小写了{"name":"durant","num":35,"abil":"四肢长"} } func main() { var player1 = Nbaplayer{"durant",35,"四肢长"} //实例化 jsonstring , _ := json.Marshal(player1) //序列化函数 json.Marshal(),返回值是一个序列化结果和一个err,忽略err值取字符串 fmt.Println(jsonstring) //是一个字节类型的串儿,无法阅读[123 34 78 97 109 101 3...] fmt.Println(string(jsonstring)) //如果想看懂用string()强转即可,{"Name":"durant","Num":35,"Abil":"四肢长"} /* json返回结构体字段注定都是变量大写,因为Golang规定大写才能被其他包使用,所以需要使用,`json:"<字段名称>"`这个就是tag方式 可以把首字母大写转成小写,这个里面用到了反射,后续再讲,现在还没学到。 */ }
方法
- 结构体中反映的东西只是一个实例的属性,都是陈述性的信息,并无法完成行为的功能,例如人吃喝拉撒睡的行为,无法用属性来描述。
- 类似python,类,属性,方法这是一套。
- Golang中的方法是作用在指定的数据类型上的,因为自定义的类,都可以有方法,而不仅仅是struct,即type出来的数据类型都可以有方法。
方法和函数的区别:
一句话概括:除了方法除了多了绑定(<随意相关参数> <绑定数据类型>)外,其他跟函数使用的方式一模一样。
类似,但是函数是 func <函数名>(参数) {…},而方法由于关联到数据类型 func (<随意相关参数> <绑定数据类型>) <函数名>(参数) {…} 区别就在这儿。
具体实例: type Cat struct {…} ; func (maomao Cat) eat() {…} //type了一个结构体,后面的方法只能由结构体类型的数据调用。
随意相关参数就是一个形参,有点类似python中的self。
详见下方代码:
package main
import "fmt"
type Persons struct {
Name string
Age int
}
func (p Persons) eat(<传参>) { //只能由某种数据类型的才可以调用的方法,其他实例不可以调用
p.Name = "阿猫"
fmt.Println("eat up lots of shit~",p.Name) //调用方法的时候使用的是值拷贝,即副本拷贝,方法内部变量的改变并不会改变main函数中的实例的值
}
func main() {
var p1 Persons = Persons{"阿狗",3} //创建一个实例
p1.eat() //实例使用一个方法,仅仅是Persons的生成的实例可以使用,调用函数所以输出阿猫
fmt.Println(p1.Name) //最后p1.Name还是阿狗
}
方法调用和传参机制原理
★★★★★方法的调用和传参机制和函数基本一样,不一样的是方法调用时,会将调用方法的变量,当做实参传递给方法。
通俗易懂的话就是说,不仅仅传递后面()中的形参,(p Person)中也会传入一个p,也就是实例p;变量是值类型就是值拷贝,引用类型则地址拷贝。
练习题:创建一个圆的属性和方法求面积
package main
import "fmt"
type Circle struct {
Radius float64
}
func (cir Circle) area(radius float64) float64 {
pi := 3.141592653
mianji := pi * radius * radius
return mianji
}
func main() {
var cir = Circle{10}
area1 := cir.area(cir.Radius) //这个cir是要传入func (cir Circle) area(radius float64) float64{}中的
fmt.Println("圆形的面积为:",area1)
}
方法注意事项和细节讨论
-
结构体类型是值类型,在方法调用中,遵守值类型传递,是值拷贝传递方式。
-
如果希望在方法中,改变结构体变量的值,则可以通过结构体指针的方式传输,结构体指针这样相比于庞大的结构体,传输值更小,效率更高,推荐!
看如下示例:package main import "fmt" type Circle struct { Radius float64 } func (cir Circle) area1(radius float64) float64 { //没啥说的,值拷贝,即便定义了radius值,也不会影响main()中 pi := 3.141592653 mianji := pi * radius * radius return mianji } func (cir *Circle) area2() float64 { //推荐使用这种方式传递结构体变量 pi := 3.141592653 //但是根据golang底层编译器设计,已经做了优化,等于所有地址,取值都不用了 (*cir).Radius = 20 //函数体内修改指针地址对应的值,main()的cri.Radius也会发生变化,写成cir.Radius也行,编译器优化 mianji := pi * (*cir).Radius * (*cir).Radius //mianji := pi * cir.Radius * cir.Radius 这样没有任何问题,cir = (*cir) return mianji } func main() { var cir = Circle{10} area1 := cir.area1(cir.Radius) fmt.Println("圆形的面积为:",area1) area2 := (&cir).area2() //同上,area2 := cir.area2()也没有任何问题,cir = (&cir),仅仅限于此,为了安全还是都表明取址取值 fmt.Println("圆形的面积为:",area2) fmt.Println(cir.Radius) //变为20了。 }
-
不仅仅是结构体,float64,int也可以有自己的方法,也可以反映值拷贝和地址拷贝
package main import "fmt" type Integer int func (i Integer) Print() { //值拷贝,修改i的值不会影响到main的值 i++ fmt.Println("i= ",i) } func (i *Integer) change() { //地址拷贝,函数内修改直接影响main中的值 *i++ fmt.Println("i= ",*i) } func main() { var i Integer = 10 i.print() fmt.Println(i) i.change() fmt.Println(i) }
-
方法与结构体,函数,变量一样,首字母大写则可在其他包中被调用。
-
如果一个类型实现了string()这个方法,那么fmt.Println()默认会调用这个变量的string()进行输出。这个确实不大好理解,但是大概知道什么意思
package main import "fmt" type Student struct { Name string Age int } func (stu *Student) String() string { str := fmt.Sprintf("name = %v , age = %v",stu.Name,stu.Age) //也就解释了第五条 return str } func main() { var stu = Student{"durant",35} fmt.Println(&stu) //自动调用了String()方法后的输出 name = durant , age = 35 fmt.Println(stu) //完全是通过main中传递,没有调用任何方法 {durant 35} }