一、Go的面向对象

  • Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。

  • Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct来实现OOP特性的。

  • Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等

  • Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承: Golang没有extends 关键字,继承是通过匿名字段来实现。

  • Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在Golang中面向接口编程是非常重要的特性。

二、struct

定义

1
2
3
4
type Cat struct {
Name string
Age int
}

在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值)
布尔类型是false ,数值是0,字符串是””。
数组类型的默认值和它的元素类型相关,比如score [3]int则为[0,0,0],指针,slice,和map的零值都是nil ,即还没有分配空间。

结构体中如果是引用类型,不要补个结构体之间赋值会改变内容

创建结构体变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1
var person Person

//2
p2 := Person{"a", 20}
//p2.Age = 18
//p2.Name = "a"
fmt.Println(p2)

//3
var p3 *Person = new(Person)
p3.Name="smith"

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

struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列号和反序列化。

问题: json处理后的字段名也是首字母大写,这样如果我们是将json后的字符串返回给其它程序使用,比如jquery,php等,那么可能他们的不习惯这个命名方式,怎么办?
解决方案

  1. 将Monster的字段首字母小写,这个行不通,你会发现处理后,返回的是空字符串,因为json.Marshal 相当于在其它包访问monster结构体,你首字母小写,就不能在其它包访问。
  2. 使用tag标签来解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"encoding/json"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
}
func useJson(){
monster := Monster{"a",16}
//字母只能是大写,否则访问不到
jsonMonster, _ := json.Marshal(monster)
//fmt.Println(jsonMonster)
//[123 34 78 97 109 101 34 58 34 97 34 44 34 65 103 101 34 58 49 54 125]
fmt.Println(string(jsonMonster))
//{"name":"a","age":16}
}

三、方法

基本介绍

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

1
2
3
4
5
6
7
8
9
10
11
type A struct {
Num int
}
func (a A) test(){
fmt.Println(a.Num)
}

func useMethod(){
var a A
a.test()
}
  1. 在通过一个变量去调用方法时,其调用机制和函数─样。不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
1
2
3
4
func (recevier type) methodName(参数列表)(返回值列表){
//方法体
//return 返回值
}
  1. 为了提高效率通常使用指针的传递
1
2
3
4
5
6
7
8
9
10
11
12
13
type Circle struct {
radius float64
}

func(c *Circle) area() float64{
return 3.14 * c.radius * c.radius
}

func test(){
var c Circle
//标准 (&c).area()
c.area()
}
  1. Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32等都可以有方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type Integer int

    func(i Integer) print(){
    fmt.Println(i)
    }

    func testInteger(){
    var i Integer = 10
    i.print()
    }
  2. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

  3. 如果一个变量实现了string()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type B struct {
    b int
    }

    func (b *B) String() string{
    str := fmt.Sprintf("b=[%v]",b.b)
    return str
    }

    func testString(){
    b := B{
    b :1,
    }
    fmt.Println(&b)
    }

方法和函数的区别

  1. 调用方式不一样函数的调用方式:函数名(实参列表)
    方法的调用方式:变量.方法名(实参列表)

  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

  3. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //这里才能觉得是值传递函数指针传递
    func (b B) testPointer(){
    b.b = 10
    fmt.Println("method",b.b)
    }

    func test02(){
    b := B{1}
    (&b).testPointer()//这里只是将指针的值传递了过去
    fmt.Println("main",b.b)
    }

    //method 10
    //main 1

    不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
    如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p*Person)则是地址拷贝。

四、工厂模式

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

1
2
3
4
package model
type Student struct {
Name string..
}

因为这里的Student的首字母s是大写的,如果我们想在其它包创建Student 的实例(比如main包),引入model包后,就可以直接创建Student结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如是type student struct …就不不行了,怎么办-工工厂模式来解决.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

type student struct {
Name string
Score float64
}

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

五、继承

继承可以解决代码复用

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

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

1
2
3
4
5
6
7
8
9
type Goods struct{
Name string
Price int
}

type Book struct{
Goods
Writer string
}
  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。

  2. 当我们直接通过b 访问字段或方法时,其执行流程如下比如b.Name,编译器会先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段,如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找..如果都找不到就报错.

  3. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分.

  4. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

    1
    2
    3
    4
    type Book struct{
    good Goods
    Writer string
    }

六、接口

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

1
2
3
4
type 接口名 interface{
method1(参数列表)返回值列表
method2(参数列表)返回值列表
}
  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
  2. Golang中的接口,不需要显式的实现。只要一个变量含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有implement这样的关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

type Usb interface {
Start()
Stop()
}

type Phone struct {

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

type Computer struct {

}
func (c Computer) Working(usb Usb){
usb.Start()
usb.Stop()
}

func testUsb(){
c := Computer{}
p := Phone{}
c.Working(p)
}
//手机开始工作
//手机停止工作

注意事项

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

}
func (stc *Stu) Say(){
fmt.Println("Say")
}

func test(){
var stu Stu = Stu{}
//报错,没有实现Stu,*Stu实现了接口
//var t Test= stu
var t Test= &stu
t.Say()
fmt.Println("here",t)
}

接口和继承

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

七、多态

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

  • 多态参数
    在前面的Usb接口案例,Usb usb,即可以接收手机变量,又可以接收相机变量,就体现了Usb接口多态
  • 多态数组
    给Usb数组中,存放Phone 结构体和Camera结构体变量

八、类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Point struct {
x int
y int
}
func useAssert() {
var a interface{}
var point Point = Point{1,2}
a = point //oK
//如何将a赋给一个Point变量?
// b = a不可以
var b Point
b = a.(Point)
fmt.Println(b)
}

将一个接口变量,赋给自定义类型的变量.=>引出类型断言.

类型断言判断

1
2
3
4
y, ok := x.(float64)
if ok {
fmt.Printf("y的类型是%T值是=%v", y. y)
}