快速入门Go(4)

Go·语法 2019-01-14 9075 字 1175 浏览 点赞

前言

这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。

此篇包括了Go相关的:匿名组合,方法,接口等。涉及关键字interface

匿名组合

Go语言没有封装、继承、多态的概念,但可以通过别的方式实现这些特性:

  • 封装:通过方法实现。
  • 继承:通过匿名字段实现。
  • 多态:通过接口实现。

匿名字段

理解Go语言中匿名字段不妨类比其他的语言的“类和继承”的概念,这里我用Python举例。

当我们需要避免一些重复工作时——比方现在我们需要两种人,一种学生,一种上班族,代码可以如下:

# 学生
class Student(object):
    name = ""
    age = 0
    gender = 1  # 1表示男生
    school = ""

# 上班族
class OfficeWorker(object):
    name = ""
    age = 0
    gender = 1
    company = ""

很好,代码写完了……欸卧槽?!怎么重复了nameagegender

事实上,但凡一个人,都有应该有名字年纪和性别。所以“学生”与“上班族”之间存在相同的成员变量不足为奇,而程序员为了“偷懒”,一拍脑袋想出了继承这一概念,于是:

class Person(object):
    name = ""
    age = 0
    gender = 1

class Student(Person):
    school = ""

class OfficeWorker(Person):
    company = ""

我们称StudentOfficeWorkerPerson的派生类,它们继承自Person。

现在Go语言的程序员也想偷懒,因此有了匿名字段,上述内容用Go实现如下:

type Person struct {
    name string
    age int
    gender byte
}

type Student struct {
    Person  // 结构体匿名字段
    school string
}

type OfficeWorker struct {
    Person
    company string
}

此时结构体Person就作为结构体Student的匿名字段。很显然,由于Person充当的是一个数据类型(声明一个Person类型的结构体语法:var p Person),所以对于结构体Student来说,数据类型为Person的变量名并不存在,那么,就“匿名”了!且Student包含Person中的所有字段。

结构体匿名字段

也就是说,这个匿名变量,其类型为结构体。因此,前面说的Person,在Student中就起到了辅助匿名变量的任务。

初始化赋值:
对于初始化一个Student类型的变量,其方法可以如下几种:

var stu = Student{Person{"ty", 22, 'm'}, "XHUnivercity" }  // 顺序初始化

var stu = Student{Person{age:22}, "XHUnivercity"}  // 对于Person来说是指定初始化,对整个Student来说,是顺序初始化

var stu = Student{Person:Person{age:22}}  // 整个Student为指定初始化

stu := Student{Person{"ty", 22, 'm'}, "XHUnivercity" }  // 简短声明

当匿名字段的数据类型为指针时,需要加上取地址符(&):

type Student struct {
    *Person  // 结构体匿名字段
    school string
}

var stu = Student{&Person{"ty", 22, 'm'}, "XHUnivercity" }  // &Person

或者用new()提前分配空间,再用点操作访问成员变量:

var stu Student
stu.Person = new(Person)
stu.name = ...
...

非初始化赋值:
非初始化赋值有三种方法:

type Person struct {
    name string
    age int
    gender byte
}

type Student struct {
    Person
    school string
}

func main() {

    var stu1, stu2, stu3 Student

    // 第一种方法
    stu1.name = "ty"
    stu1.age = 22
    stu1.gender = 'm'
    stu1.school = "XHUnivercity"
    fmt.Printf("%+v\n", stu1)

    // 第二种方法
    stu2.Person.name = "ty"
    stu2.Person.age = 22
    stu2.Person.gender = 'm'
    stu2.school = "XHUnivercity"
    fmt.Printf("%+v\n", stu2)

    // 第三种方法
    stu3.Person = Person{"ty", 22, 'm'}
    stu3.school = "XHUnivercity"
    fmt.Printf("%+v\n", stu3)
}
// 输出:
{Person:{name:ty age:22 gender:109} school:XHUnivercity}
{Person:{name:ty age:22 gender:109} school:XHUnivercity}
{Person:{name:ty age:22 gender:109} school:XHUnivercity}

同名字段

在两个具有“父子”关系的类中,如果出现相同名字的成员变量,子类的实例化对象会优先访问子类中成员变量。这样的结论也适用于Go语言。

首先,当一个结构体中的使用了另一个结构体,且两个结构体中存在相同名称的成员变量,我们称这个变量为同名字段。就像下边的salary一样:

type Boyfriend struct{
    salary int
}

type Girlfrien struct{
    Boyfriend
    salary int
}

func main() {
    var girl = Girlfrien{Boyfriend{30000}, 30000}
    fmt.Printf("%+v\n", girl)

    var bagPrice  =  4000
    girl.salary -= bagPrice  // 女生要买包包了(直接点操作)
    fmt.Printf("%+v\n", girl)
}
// 输出:
{Boyfriend:{salary:30000} salary:30000}
{Boyfriend:{salary:30000} salary:26000}

这是可以理解的:尽管男友每个月上交工资,但女生仍要清楚,自己仅仅拥有随意花自己钱的权利。所以直接点操作访问的是自己的salary,如果需要用男友的钱买包包,我建议经过男友的同意比较好:

girl.Boyfriend.salary -= bagPrice  // 使用 Boyfriend 中的salary

非结构体匿名字段

并非匿名字段的数据类型只能是结构体,基础类型、自定义类型也可以:

type mystr string

type Person struct{
    name string
}

type Student struct {
    Person  // 结构体匿名字段
    int  // 基础类型匿名字段
    mystr  // 自定义类型匿名字段
}

func main() {
    stu := Student{Person{"ty"}, 22, "XHUnivercity"}
    fmt.Printf("%+v\n", stu)
    // 也可以
    fmt.Println(stu.Person, stu.int, stu.mystr)  // 只有匿名字段可以 点 + 数据类型
}
// 输出:
{Person:{name:ty} int:22 mystr:XHUnivercity}
{ty} 22 XHUnivercity

需要注意两点:

  • 只有匿名字段可以 点 + 数据类型方式的访问;
  • 对匿名字段的要求:一个结构体中不应该存在相同类型的匿名字段。

方法

什么是方法

大部分语言中方法和函数是相同的概念,Go语言却不是,快速入门Go2 中提到的是函数,格式如:
func funcName(paramsList) (returnList) {}

方法格式为:
func (receiver ReceiverType) funcName(paramsList) (returnList) {}

Go语言中,可以给任意自定义类型添加相应的方法。比方说:

type zty string

func (z zty) greet(words string) {
    fmt.Printf("%s\n", words)
}

func main() {
    var z zty
    z.greet("hello world")  // 对于类型为zty的变量来说,它们都拥有了greet()这个方法
}
// 输出:
hello world

如果方法配合结构体一起使用,有没有很像其他语言中的类呢!

关于方法的使用需要注意以下几点:

  • 参数receiver可以任意命名,如果方法中未曾使用,可省略参数名。
  • 参数receiver类型可以是T或*T。基类型T不能是接口或指针。
  • 不支持重载。(只要接收者类型不一样,此时认为T与*T是同一种类型,这个方法就算同名也是不同的方法,不会出现重复定义函数的错误。)

值语义和引用语义

值语义表示方法的接收者为普通变量,非指针。即,通过拷贝方式传入:

type student struct{
    name string
    age int
    id int64
}

func (s student) setInfoValueByValue(name string, age int, id int64) {
    s.name = name
    s.age = age
    s.id = id
}

func main() {
    var s student
    s.setInfoValueByValue("ty", 22, 201431060011)
    fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id)
}
// 输出:
name = , age = 0, id = 0

引用语义表示接收者为指针类型的变量,利用引用传递:

type student struct{
    name string
    age int
    id int64
}

func (s *student) setInfoValueByPointer(name string, age int, id int64) {
    s.name = name
    s.age = age
    s.id = id
}

func main() {
    var s student
    p := &s
    p.setInfoValueByPointer("ty", 22, 201431060011)

    fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id)
}
// 输出:
name = ty, age = 22, id = 201431060011

所以二者最大的区别在于:能不能通过方法来改变自身的值。

方法集

即:可以被该类型的值调用的所有方法的集合。

由于Go语言中的指针与普通变量使用起来很灵活,所以可以通过转换调用彼此的方法:

type me string

func (m me) PrintAlpha() {
    fmt.Println("a, b, c")
}

func (m *me) PrintDigit() {
    fmt.Println("1, 2, 3")
}

func main() {
    var m me
    m.PrintAlpha()
    (&m).PrintDigit()
    fmt.Println("-------- 分割线 -------- ")
    p := new(me)
    (*p).PrintAlpha()
    p.PrintDigit()
}
// 输出:
a, b, c
1, 2, 3
-------- 分割线 -------- 
a, b, c
1, 2, 3

事实上编译器可以自动识别并做类型转换,所以上述代码可以修改两句:

(&m).PrintDigit()
// ...
(*p).PrintAlpha()

// 改为
m.PrintDigit()
// ...
p.PrintAlpha()

方法的继承

当结构体B中包含了结构体A,那么结构体B就拥有了结构体A中的所有方法。

方法的重写

当结构体B中包含了结构体A,为结构体B绑定的方法恰好与结构体A中的方法同名,此时认为重写了这个方法。在调用过程中,结构体B类型的变量会优先调用结构体B上绑定的方法,如果没有,才会去结构体A中寻找。

方法表达式

即:记录了指定方法的地址的指针。使用方法表达式时,第一个参数一定是这个方法的接收者类型的变量。

func (me) greet(words string) {
    fmt.Println(words)
}

func main() {
    var m me
    p := (me).greet  // p为方法表达式
    p(m, "hello world")  // 使用方法表达式,第一个参数一定是这个方法的接收者类型的变量
}
// 输出:
hello world

接口

Go语言中,接口是一个自定义类型,作为描述一系列方法的集合。关于接口需要注意以下几点:

  • 接口命名习惯以er结尾。
  • 接口只有方法声明,没有实现,没有数据字段。
  • 接口可以匿名嵌入其他接口,或嵌入到结构中。

接口的定义

接口的定义类似结构体定义:

type Humaner interface {
    // ...
}

接口的使用

利用接口,可以很好的实现多态:

type Humaner interface {
    sayWords()  // 方法声明
}

type (
    personOne string
    personTwo string
)

func (personOne) sayWords() {
    fmt.Printf("我是 personOne,很高兴认识大家\n")
}

func (personTwo) sayWords() {
    fmt.Printf("在下 personTwo,庆幸相遇\n")
}

// 注意参数类型
func sayWords(i Humaner) {
    i.sayWords()
}

func main() {
    var (
        one personOne
        two personTwo
    )
    // 同一个函数,实现不同的功能
    sayWords(one)
    sayWords(two)
}
// 输出:
我是 personOne,很高兴认识大家
在下 personTwo,庆幸相遇

接口的继承与结构体相似,接口继承又被称为接口嵌用。

接口的转换

Go语言中,作为超集的接口可以转化为子集接口,反之却不行。我们对超、子集接口有如下定义:

type Oner interface {  // 子集
    sayWords()
}

type Twoer interface {  // 超集,比子集 Oner 多了一个sing()方法
    Oner  // 匿名嵌入
    sing()
}

所以此时可以超集转子集:

var one Oner
one = me("one")
var two Twoer
two = me("two")

one = two  // 正确
one.sayWords()

绝不可以子转超

two = one  // 错误
two.sayWords()

空接口

你可以将空接口这个概念认为是C语言中的void *,因为空接口可以存储任意类型的值(interface{})。

func main() {
    var i interface{} = 1
    fmt.Printf("类型:%T, 值:%v\n", i, i)
    i = "abc"
    fmt.Printf("类型:%T, 值:%v\n", i, i)
    i = 5.12
    fmt.Printf("类型:%T, 值:%v\n", i, i)
}
// 输出:
类型:int, 值:1
类型:string, 值:abc
类型:float64, 值:5.12

因为空接口可以存储任意类型的变量,而有时候我们又需要知道变量的类型,此时就有了类型断言的概念。一般而言有两种方法,第一种利用if ... else if...,第二种switch

// if ... else if ...
func main() {
    var i interface{} = 1
    
    if value, ok := i.(string); ok {
        fmt.Printf("i 的类型是string, i 的值是 %s", value)
    } else if value, ok := i.(int); ok {
        fmt.Printf("i 的类型是int, i 的值是 %d", value)
    }
}
func main() {
    var i interface{} = 1
    
    switch value := i.(type) {  // .(type) 这样的操作只能在switch中使用
    case int:
        fmt.Printf("i 的类型是int, i 的值是 %d", value)
    case string:
        fmt.Printf("i 的类型是string, i 的值是 %s", value)
    }
}


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论