Go核心开发学习笔记(廿一) —— 结构体sturct,方法

面向对象编程(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)
}

结构体内存分配机制

  1. 两个结构体实例:p1,p2, p2 = p1,修改p2的值,不会影响p1,符合值拷贝,不再过多赘述。
  2. 如果 p2是指针类型结构体,那么 p2 = &p1,则修改p2中的结构体字段值,会影响p1,符合之前引用传递,大同小异。
  3. 结构体所有字段在内存中是连续分布的,可以通过地址加减来获得不同的字段对应的值。
  4. 嵌套型结构体,外层结构体如果是指针类型,那么外层结构体指针的地址一定是连续的,但是指向的内层结构体的值不一定连续,这个需要根据计算机实际情况来算。

结构体强转

  1. 可以转换,但是要求是 两个结构体的字段和类型要完全相同,名字个数类型都必须一致!
  2. 定义一个结构体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...

结构体使用细节和原理

  1. 一个Go语言写的应用,客户端进行访问,Go语言返回客户端的需求信息,一般将结构体序列化成json格式(字符串)返回给服务器。
  2. 序列化需要 import “encoding/json”,注意事项详见下面代码,今后开发会遇到很多 `json:"<字段名称>"` 的方式。
  3. 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方式
    	可以把首字母大写转成小写,这个里面用到了反射,后续再讲,现在还没学到。
    	 */
    }
    

方法

  1. 结构体中反映的东西只是一个实例的属性,都是陈述性的信息,并无法完成行为的功能,例如人吃喝拉撒睡的行为,无法用属性来描述。
  2. 类似python,类,属性,方法这是一套。
  3. 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)
}

方法注意事项和细节讨论

  1. 结构体类型是值类型,在方法调用中,遵守值类型传递,是值拷贝传递方式。

  2. 如果希望在方法中,改变结构体变量的值,则可以通过结构体指针的方式传输,结构体指针这样相比于庞大的结构体,传输值更小,效率更高,推荐!
    看如下示例:

    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了。
    }
    
  3. 不仅仅是结构体,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)
    }
    
  4. 方法与结构体,函数,变量一样,首字母大写则可在其他包中被调用。

  5. 如果一个类型实现了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}
    }
    
发布了49 篇原创文章 · 获赞 18 · 访问量 4006

猜你喜欢

转载自blog.csdn.net/weixin_41047549/article/details/89973103
今日推荐