Golang学习+深入(十)-面向“对象“编程

目录

一、概述

1、结构体

1.1、创建结构体变量和访问结构体变量

1.2、结构体的注意事项和使用细节

1.3、创建结构体变量时指定字段值

1.4、工厂模式

1.5、抽象

1.6、面向对象编程三大特性

2、方法

2.1、方法注意事项和细节讨论

3、接口(interface)

3.1、注意事项和细节

3.2、接口排序

3.3、接口VS继承


一、概述

1、结构体

一个程序就是一个世界,有很多对象(变量)

  1. Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
  2. Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位。
  3. Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等。
  4. Golang仍然有面向对象编程的继承、封装和多态的特性,只是实现的方式和其他OOP语言不一样,比如继承:Golang没有extends关键字,继承是通过匿名字段来实现。
  5. Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。在Golang中面向接口编程是非常重要的特性。
package main
import (
	"fmt"
)

type Cat struct {
	Name string
	Age int
	Color string
}

func main(){
	var cat1 Cat
	cat1.Name="小白"
	cat1.Age=3
	cat1.Color="白色"
	fmt.Println("cat1=",cat1)
	fmt.Println("cat1 Name=",cat1.Name)
}

结构体是自定义的数据类型,代表一类事物

声明结构体
type 结构体名称 struct {
    field1 type
    field2 type
}
例:
type Cat struct {
	Name string
	Age int
	Color string
}
  1. 字段的类型可以为:基本类型,数组或引用类型
  2. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值)
  3. 数组的默认值和它的元素类型相关,score[3]int 则为[0,0,0]
  4. 指针,slice,map的零值都是nil,即还没有分配内存空间
  5. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个。
package main
import (
	"fmt"
)
func main(){
	var p1 Person
	fmt.Println("p1=",p1)
	p1.Name="小明"
	p1.Age=10
	p1.slice= make([]int,10)
	p1.map1 = make(map[string]string)
	p1.map1["k1"] = "v1"
	fmt.Println("p1=",p1)

}
type Person struct {
	Name string
	Age int
	map1 map[string]string
	slice []int
	ptr *int //指针
}

1.1、创建结构体变量和访问结构体变量

方式1:
    var person Person
方式2:
    var person Person = Person{}
例:
package main
import (
	"fmt"
)
type Person struct {
	Name string
	Age int
}
func main(){
	p := Person{"mary",20}
	fmt.Println(p)
}
方式3:
    var person *Person = new(Person)
    (*person).Name="tom" //等价 person.Name="tom"  因为go编译器底层对person.Name做了转化(*person).Name
    (*person).Age=30
    fmt.Println(*person)
方式4:
    var person *Person = &Person{}//var person *Person = &Person{"tom",60}
    (*person).Name="tom" //等价 person.Name="tom"  因为go编译器底层对person.Name做了转化(*person).Name
    person.Name="tom--"
    (*person).Age=30
    person.Age=10
    fmt.Println(*person)

说明:
1.第三种和第四种方式返回的是 结构体指针。
2.结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 如:(*person).Name ="tom"
3.但go做了一个简化,也支持结构体指针.字段名,比如 person.Name = "tom" 。
更加符合程序员使用的习惯,go编译器底层对person.Name做了转化(*person).Name

请问可以这样写吗?
*person.Name:不可以,会报错。因为.的运算符优先级比*高。

1.2、结构体的注意事项和使用细节

  1. 结构体的所有字段在内存中是连续的
  2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main
import (
	"fmt"
)

type A struct {
	Num int
}

type B struct {
	Num int
}

func main(){
	var a A
	var b B
	
	b = B(a)
	fmt.Println(b)
}
  1. 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
  2. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见使用场景就是序列化和反序列化。
import "encoding/json"
func Marshal(v interface{}) ([]byte, error):Marshal函数返回v的json编码。
======================================================
package main
import (
	"fmt"
	"encoding/json"
)
type C struct {
	Name string `json:"name"` //`json:"name"`结构体标签
	Age int `json:"age"`
	Skill string `json:"skill"`
}
func main(){
	c :=C{"孙悟空",1000,"金箍棒"}
	cJson,err :=json.Marshal(c) //json.Marshal函数中使用到反射,把tag标签json名称替换掉本来的属性名
	if err !=nil{
		fmt.Println("err=",err)
	}
	fmt.Println("cJson",string(cJson))
}

1.3、创建结构体变量时指定字段值

Golang在创建结构体实例(变量)时,可以直接指定字段的值。

package main
import (
	"fmt"
)
type Student struct {
	Name string
	Age int
}
func main(){
	var stu1 Student=Student{"tom",10}
	stu2 := Student{"tom",20}
	var stu3 Student=Student{
		Name:"tom",
		Age:30,
	}
	stu4 :=Student{
		Name:"tom",
		Age:40,
	}
	var stu5 *Student=&Student{"stu5",50}
	var stu6 *Student=&Student{
		Name:"tom",
		Age:60,
	}
	fmt.Println(stu1)
	fmt.Println(stu2)
	fmt.Println(stu3)
	fmt.Println(stu4)
	fmt.Println(stu5)
	fmt.Println(stu6)
}

1.4、工厂模式

Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。

使用工厂模式实现跨包创建结构体实例(变量)

  1. 如果model包的 结构体变量首字母大写,引入后,直接使用,没有问题
  2. 如果model包的 结构体变量首字母小写,引入后,不能直接使用,可以使用工厂模式解决
package model  
import (
	_ "fmt"
)
type student struct {
	Name string
	Score float64
}
func NewStudent(n string,s float64) *student {
	return &student{
		Name : n,
		Score : s,
	}
}
================================
package main
import (
	"fmt"
	"model"    //model包下的代码我整体拷贝到GO安装环境目录下面,可调用
)
func main(){
	var stu = model.NewStudent("张无忌",98.0)
	fmt.Println(stu) //&{张无忌 98}
}

//如果Score改为小写开头score,则在其他包不可以直接访问 //提供一个对外的方法

package model
import (
	_ "fmt"
)

type student struct {
	Name string
	score float64
}

func NewStudent(n string,s float64) *student {
	return &student{
		Name : n,
		score : s,
	}
}

//如果Score改为小写开头score,则在其他包不可以直接访问
//提供一个对外的方法
func (s *student) GetScore() float64 {
	return s.score
}
-----------------
package main
import (
	"fmt"
	"model"
)

func main(){
	var stu = model.NewStudent("张无忌",98.0)
	fmt.Println(stu.GetScore())//98
}

1.5、抽象

定义一个结构体的时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(模板)。这种研究问题的方法称为抽象。

1.6、面向对象编程三大特性

Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现方式和其他OOP语言不一样。

封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作。对电视机的操作就是典型的封装。

封装的理解和好处:

  1. 隐藏实现细节
  2. 可以对数据进行验证,保证安全合理

如何体现封装:

  1. 对结构体中的属性进行封装
  2. 通过方法,包 实现封装

封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出了,其他包不能使用,类似private)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的Set方法(类似其他语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表){
    //添加验证逻辑
    var.字段=参数
}
  1. 提供一个首字母大写的Get方法(类似其他语言的public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
    return var.字段
}
  1. 在Golang开发中并没有特别强调封装,这点并不像Java。

继承

继承可以解决代码复用,让我们的编程更加靠近人类思维

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。

其他的结构体不需要重新定义这些属性和方法,只需要嵌套一个共通定义的匿名结构体即可。

也就是说,在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

type Goods struct{
    Name string
    Price int
}

type Book struct{
    Goods //这里就是嵌套匿名结构体Goods
    Write string
}
  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
  2. 匿名结构体访问可以简化
var book Book
book.Goods.Name="tom"
↓
book.Name="tom"
  1. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
  2. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段或方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
  3. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字。
  4. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值。

多态

变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

参考下面接口中的Usb案例

类型断言

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言。

package main
import (
	"fmt"
)

func main(){
	var t float32
	var x interface{}

	x=t
	_,ok :=x.(float32)
	if ok ==true{
		fmt.Println("success")
	}else{
		fmt.Println("fail")
	}
}

2、方法

Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。

方法的声明和调用
func (变量名 type) methodName(参数列表) (返回值列表) {
    方法体
    return 返回值
}
1.变量名 type:表示这个方法和type这个类型进行绑定,或者该方法作用于type类型。
2.返回值列表:表示返回的值,可以多个
3.return 语句不是必须的

type A struct {
    Num int
}
func (a A) test(){
    fmt.Println(a.Num)
}
例:
var t A
t.test()

方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当作实参也传递给方法。

package main
import (
	"fmt"
)
type Circle struct {
	radius float64
}
func (c Circle) area() float64 {
	return 3.14 * c.radius * c.radius
}
func main(){
	var c Circle
	c.radius =2.0
	res := c.area()
	fmt.Println("res=",res)
}

2.1、方法注意事项和细节讨论

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  3. Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。比如:int,float32等也可以定义方法
  4. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其他包访问
  5. 如果一个变量实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出

方法和函数的区别

  1. 调用方式不一样
    1. 函数的调用方式:函数名(实参列表)
    2. 方法的调用方式:变量.方法名(实参列表)
  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  3. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

3、接口(interface)

USB插槽就是现实中的接口。

Golang中 多态特性主要是通过接口来体现的。

interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型要使用的时候,再根据具体情况把这些方法写出来。

基本语法:
type 接口名 interface{
    method1(参数列表) 返回值列表
    method2(参数列表) 返回值列表
    ...
}
  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想。
  2. Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字。
  3. 实现了接口就是指实现了接口声明的所有方法
package main
import (
	"fmt"
)
type Usb interface {
	Start()
	Stop()
}
type Phone struct {

}
func (phone Phone) Start(){
	fmt.Println("手机开始工作...")
}
func (phone Phone) Stop(){
	fmt.Println("手机停止工作...")
}
type Camera struct {

}
func (camera Camera) Start(){
	fmt.Println("相机开始工作...")
}
func (camera Camera) Stop(){
	fmt.Println("相机停止工作...")
}
type Computer struct {

}
//实现了Usb接口就是指实现了Usb接口声明的所有方法
func (computer Computer) working(usb Usb){
	usb.Start()
	usb.Stop()
}
func main(){
	computer :=Computer{}
	phone :=Phone{}
	camera := Camera{}
	computer.working(phone)
	computer.working(camera)
}

3.1、注意事项和细节

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
  2. 接口中所有的方法都没有方法体,即都是没有实现的方法
  3. 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
  4. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
  5. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
  6. 一个自定义类型可以实现多个接口
  7. Golang接口中不能有任何变量
  8. 一个接口(比如A接口)可以继承多个别的接口(比如B、C接口),这时如果要实现A接口,也必须将B、C接口的方法也全部实现。
  9. interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
  10. 空接口interface{}没有任何方法,所以所有类型都实现了空接口

3.2、接口排序

import "sort"
func Sort(data Interface):Sort排序data,需要实现接口下的三个方法
type Interface interface {
    // Len方法返回集合中的元素个数
    Len() int
    // Less方法报告索引i的元素是否比索引j的元素小
    Less(i, j int) bool
    // Swap方法交换索引i和j的两个元素
    Swap(i, j int)
}
=========================================
package main
import (
	"fmt"
	"sort"
)

func main(){
	var intSlice = []int{0,-1,10,7,90,23}
	sort.Ints(intSlice)
	fmt.Println(intSlice)
}

3.3、接口VS继承

  1. 实现接口可以看作是对继承的一种补充。
  2. 接口和继承解决的问题不同
  3. 继承的价值主要在于:解决代码的复用性和可维护性。
  4. 接口的价值在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法
  5. 接口比继承更加灵活
  6. 接口比继承更加灵活,继承是满足 is - a的关系,而接口只需满足like - a的关系
  7. 接口在一定程度上实现代码解耦

干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!

你可能感兴趣的:(golang,学习,java)