第二章:面向对象

1 面向对象:结构体与继承

1.1 什么是结构体?

在之前学过的数据类型中,数组与切片,只能存储同一类型的变量。若要存储多个类型的变量,就需要用到结构体,它是将多个任意类型的变量组合在一起的聚合数据类型。
每个变量都成为该结构体的成员变量。
可以理解为 Go语言 的结构体struct和其他语言的class有相等的地位,但是Go语言放弃大量面向对象的特性,所有的Go语言类型除了指针类型外,都可以有自己的方法,提高了可扩展性。
在 Go 语言中没有没有 class 类的概念,只有 struct 结构体的概念,因此也没有继承,本篇文章,带你学习一下结构体相关的内容。

1.2 定义结构体

声明结构体

type 结构体名 struct {
    属性名   属性类型
    属性名   属性类型
    ...
}

比如我要定义一个可以存储个人资料名为 Profile 的结构体,可以这么写

type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

若相邻的属性(字段)是相同类型,可以合并写在一起

type Profile struct {
    name,gender   string  // 这里合并在了一起
    age    int
    mother *Profile // 指针
    father *Profile // 指针
}

通过结构体可以定义一个组合字面量,有几个细节,也算是规则,需要你注意。
规则一:当最后一个字段和结果不在同一行时,, 不可省略。

xm := Profile{
    name: "小明",
    age: 18,
    gender: "male",
}

反之,在同一行,就可以省略。

xm := Profile{
    name: "小明",
    age: 18,
    gender: "male"}

规则二:字段名要嘛全写,要嘛全不写,不能有的写,有的不写。

xm := Profile{
    name: "小明",
    18,
    "male",
}

例如下面这种写法是会报 mixture of field:value and value initializers 错误的
规则三:初始化结构体,并不一定要所有字段都赋值,未被赋值的字段,会自动赋值为其类型的零值。

xm := Profile{name: "小明"}
fmt.Println(xm.age)
// output: 0

但要注意的是,只有通过指定字段名才可以赋值部分字段。
若你没有指定字段名,像这样

xm := Profile{"小明"}

在编译的时候,是会直接报错的

$ go run demo.go
# command-line-arguments
./demo.go:12:16: too few values in Profile literal

1.3 绑定方法

在 Go 语言中,我们无法在结构体内定义方法,那如何给一个结构体定义方法呢,答案是可以使用组合函数的方式来定义结构体方法。它和普通函数的定义方式有些不一样,比如下面这个方法

func (person Profile) FmtProfile() {
    fmt.Printf("名字:%s\n", person.name)
    fmt.Printf("年龄:%d\n", person.age)
    fmt.Printf("性别:%s\n", person.gender)
}

其中FmtProfile 是方法名,而(person Profile) :表示将 FmtProfile 方法与 Profile 的实例绑定。我们把 Profile 称为方法的接收者,而 person 表示实例本身,它相当于 Python 中的 self,在方法内可以使用 person.属性名 的方法来访问实例属性。
完整代码如下:

package main

import "fmt"

// 定义一个名为Profile 的结构体
type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

// 定义一个与 Profile 的绑定的方法
func (person Profile) FmtProfile() {
    fmt.Printf("名字:%s\n", person.name)
    fmt.Printf("年龄:%d\n", person.age)
    fmt.Printf("性别:%s\n", person.gender)
}

func main() {
    // 实例化
    myself := Profile{name: "小明", age: 24, gender: "male"}
    // 调用函数
    myself.FmtProfile()
}

输出如下

名字:小明
年龄:24
性别:male

1.4 方法的参数传递方式

当你想要在方法内改变实例的属性的时候,必须使用指针做为方法的接收者。

package main

import "fmt"

// 声明一个 Profile 的结构体
type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

// 重点在于这个星号: *
func (person *Profile) increase_age() {
    person.age += 1
}

func main() {
    myself := Profile{name: "小明", age: 24, gender: "male"}
    fmt.Printf("当前年龄:%d\n", myself.age)
    myself.increase_age()
    fmt.Printf("当前年龄:%d", myself.age)
}

输出结果 如下,可以看到在方法内部对 age 的修改已经生效。你可以尝试去掉 *,使用值做为方法接收者,看看age是否会发生改变(答案是:不会改变)

当前年龄:24
当前年龄:25

至此,我们知道了两种定义方法的方式:

  • 以值做为方法接收者
  • 以指针做为方法接收者

那我们如何进行选择呢?以下几种情况,应当直接使用指针做为方法的接收者。

  1. 你需要在方法内部改变结构体内容的时候
  2. 出于性能的问题,当结构体过大的时候

有些情况下,以值或指针做为接收者都可以,但是考虑到代码一致性,建议都使用指针做为接收者。
不管你使用哪种方法定义方法,指针实例对象、值实例对象都可以直接调用,而没有什么约束。这一点Go语言做得非常好。

1.5 结构体实现 “继承”

为什么标题的继承,加了双引号,因为Go 语言本身并不支持继承。
但我们可以使用组合的方法,实现类似继承的效果。
在生活中,组合的例子非常多,比如一台电脑,是由机身外壳,主板,CPU,内存等零部件组合在一起,最后才有了我们用的电脑。
同样的,在 Go 语言中,把一个结构体嵌入到另一个结构体的方法,称之为组合。
现在这里有一个表示公司(company)的结构体,还有一个表示公司职员(staff)的结构体。

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
}

若要将公司信息与公司职员关联起来,一般都会想到将 company 结构体的内容照抄到 staff 里。

type staff struct {
    name string
    age int
    gender string
    companyName string
    companyAddr string
    position string
}

虽然在实现上并没有什么问题,但在你对同一公司的多个staff初始化的时候,都得重复初始化相同的公司信息,这做得并不好,借鉴继承的思想,我们可以将公司的属性都“继承”过来。
但是在 Go 中没有类的概念,只有组合,你可以将 company 这个 结构体嵌入到 staff 中,做为 staff 的一个匿名字段,staff 就直接拥有了 company 的所有属性了。

type staff struct {
    name string
    age int
    gender string
    position string
    company   // 匿名字段
}

来写个完整的程序验证一下。

package main

import "fmt"

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
    company
}

func main()  {
    myCom := company{
        companyName: "Tencent",
        companyAddr: "深圳市南山区",
    }
    staffInfo := staff{
        name:     "小明",
        age:      28,
        gender:   "男",
        position: "云计算开发工程师",
        company: myCom,
    }

    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.companyName)
    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.company.companyName)
}

输出结果如下,可见staffInfo.companyName 和 staffInfo.company.companyName 的效果是一样的。

小明 在 Tencent 工作
小明 在 Tencent 工作

1.6 内部方法与外部方法

在 Go 语言中,函数名的首字母大小写非常重要,它被来实现控制对方法的访问权限。

  • 当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用
  • 当方法的首字母为小写时,这个方法是Private,其他包是无法访问的。

1.7 三种实例化方法

第一种:正常实例化

func main() {
    xm := Profile{
        name: "小明",
        age: 18,
        gender: "male",
    }
}

第二种:使用 new

func main() {
    xm := new(Profile)
    // 等价于: var xm *Profile = new(Profile)
    fmt.Println(xm)
    // output: &{ 0 }

    xm.name = "iswbm"   // 或者 (*xm).name = "iswbm"
    xm.age = 18     //  或者 (*xm).age = 18
    xm.gender = "male" // 或者 (*xm).gender = "male"
    fmt.Println(xm)
    //output: &{iswbm 18 male}
}

第三种:使用 &

func main() {
    var xm *Profile = &Profile{}
    fmt.Println(xm)
    // output: &{ 0 }

    xm.name = "iswbm"   // 或者 (*xm).name = "iswbm"
    xm.age = 18     //  或者 (*xm).age = 18
    xm.gender = "male" // 或者 (*xm).gender = "male"
    fmt.Println(xm)
     //output: &{iswbm 18 male}
}

1.8 选择器的冷知识

从一个结构体实例对象中获取字段的值,通常都是使用 . 这个操作符,该操作符叫做 选择器
选择器有一个妙用,可能大多数人都不清楚。
当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做

type Profile struct {
    Name string
}

func main() {
    p1 := &Profile{"iswbm"}
  fmt.Println((*p1).Name)  // output: iswbm
}

但还有一个更简洁的做法,可以直接省去 * 取值的操作,选择器 . 会直接解引用,示例如下

type Profile struct {
    Name string
}

func main() {
    p1 := &Profile{"iswbm"}
    fmt.Println(p1.Name)  // output: iswbm
}

2 面向对象:接口与多态

2.1 接口是什么?

这一段摘自 Go语言中文网
在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。
在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法

2.2 如何定义接口

使用 type 关键字来定义接口。
如下代码,定义了一个电话接口,接口要求必须实现 call 方法。

type Phone interface {
   call()
}

2.3 如何实现接口

如果有一个类型/结构体,实现了一个接口要求的所有方法,这里 Phone 接口只有 call方法,所以只要实现了 call 方法,我们就可以称它实现了 Phone 接口。
意思是如果有一台机器,可以给别人打电话,那么我们就可以把它叫做电话。
这个接口的实现是隐式的,不像 JAVA 中要用 implements 显示说明。
继续上面电话的例子,我们先定义一个 Nokia 的结构体,而它实现了 call 的方法,所以它也是一台电话。

type Nokia struct {
    name string
}

// 接收者为 Nokia
func (phone Nokia) call() {
    fmt.Println("我是 Nokia,是一台电话")
}

2.4 接口实现多态

鸭子类型(Duck typing)的定义是,只要你长得像鸭子,叫起来也像鸭子,那我认为你就是一只鸭子。
举个通俗的例子
什么样子的人可以称做老师呢?
不同的人标准不一,有的人认为必须有一定的学历,有的人认为必须要有老师资格证。
而我认为只要能育人,能给传授给其他人知识的,都可以称之为老师。
而不管你教的什么学科?是体育竞技,还是教人烹饪。
也不管你怎么教?是在教室里手执教教鞭、拿着粉笔,还是追求真实,直接实战演练。
通通不管。
这就一个接口(老师)下,在不同对象(人)上的不同表现。这就是多态。
在 Go 语言中,是通过接口来实现的多态。
这里以商品接口来写一段代码演示一下。
先定义一个商品(Good)的接口,意思是一个类型或者结构体,只要实现了settleAccount() 和 orderInfo() 两个方法,那这个类型/结构体就是一个商品。

type Good interface {
    settleAccount() int
    orderInfo() string
}

然后我们定义两个结构体,分别是手机和赠品。

type Phone struct {
    name string
    quantity int
    price int
}

type FreeGift struct {
    name string
    quantity int
    price int
}

然后分别为他们实现 Good 接口的两个方法

// Phone
func (phone Phone) settleAccount() int {
    return phone.quantity * phone.price
}
func (phone Phone) orderInfo() string{
    return "您要购买" + strconv.Itoa(phone.quantity)+ "个" +
        phone.name + "计:" + strconv.Itoa(phone.settleAccount()) + "元"
}

// FreeGift
func (gift FreeGift) settleAccount() int {
    return 0
}
func (gift FreeGift) orderInfo() string{
    return "您要购买" + strconv.Itoa(gift.quantity)+ "个" +
        gift.name + "计:" + strconv.Itoa(gift.settleAccount()) + "元"
}

实现了 Good 接口要求的两个方法后,手机和赠品在Go语言看来就都是商品(Good)类型了。
这时候,我挑选了两件商品(实例化),分别是手机和耳机(赠品,不要钱)

iPhone := Phone{
    name:     "iPhone",
    quantity: 1,
    price:    8000,
}
earphones := FreeGift{
    name:     "耳机",
    quantity: 1,
    price:    200,
}

然后创建一个购物车(也就是类型为 Good的切片),来存放这些商品。

goods := []Good{iPhone, earphones}

最后,定义一个方法来计算购物车里的订单金额

func calculateAllPrice(goods []Good) int {
    var allPrice int
    for _,good := range goods{
        fmt.Println(good.orderInfo())
        allPrice += good.settleAccount()
    }
    return allPrice
}

完整代码,我贴在下面,供你参考。

package main

import (
    "fmt"
    "strconv"
)

// 定义一个接口
type Good interface {
    settleAccount() int
    orderInfo() string
}

type Phone struct {
    name string
    quantity int
    price int
}

func (phone Phone) settleAccount() int {
    return phone.quantity * phone.price
}
func (phone Phone) orderInfo() string{
    return "您要购买" + strconv.Itoa(phone.quantity)+ "个" +
        phone.name + "计:" + strconv.Itoa(phone.settleAccount()) + "元"
}

type FreeGift struct {
    name string
    quantity int
    price int
}

func (gift FreeGift) settleAccount() int {
    return 0
}
func (gift FreeGift) orderInfo() string{
    return "您要购买" + strconv.Itoa(gift.quantity)+ "个" +
        gift.name + "计:" + strconv.Itoa(gift.settleAccount()) + "元"
}

func calculateAllPrice(goods []Good) int {
    var allPrice int
    for _,good := range goods{
        fmt.Println(good.orderInfo())
        allPrice += good.settleAccount()
    }
    return allPrice
}
func main()  {
    iPhone := Phone{
        name:     "iPhone",
        quantity: 1,
        price:    8000,
    }
    earphones := FreeGift{
        name:     "耳机",
        quantity: 1,
        price:    200,
    }

    goods := []Good{iPhone, earphones}
    allPrice := calculateAllPrice(goods)
    fmt.Printf("该订单总共需要支付 %d 元", allPrice)
}

运行后,输出如下

您要购买1个iPhone计:8000元
您要购买1个耳机计:0元
该订单总共需要支付 8000 元

3 面向对象:结构体里的 Tag 用法

3.1 抛砖引玉:什么是 Tag?

正常情况下,你定义的结构体是这样子的,每个字段都由名字和字段类型组成

type Person struct {
    Name string
    Age  int
    Addr string
}

也有例外,就像下面这样子,字段上还可以额外再加一个属性,用反引号(Esc键下面的那个键)包含的字符串,称之为 Tag,也就是标签。

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Addr string `json:"addr,omitempty"`
}

那么这个标签有什么用呢?
这边先以 encoding/json 库的用法抛砖引玉,看一下它能起到什么样的效果。
由于 Person 结构体里的 Addr 字段有 omitempty 属性,因此 encoding/json 在将对象转化 json 字符串时,只要发现对象里的 Addr 为 false, 0, 空指针,空接口,空数组,空切片,空映射,空字符串中的一种,就会被忽略。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Addr string `json:"addr,omitempty"`
}

func main() {
    p1 := Person{
        Name: "Jack",
        Age:  22,
    }

    data1, err := json.Marshal(p1)
    if err != nil {
        panic(err)
    }

    // p1 没有 Addr,就不会打印了
    fmt.Printf("%s\n", data1)

    // ================

    p2 := Person{
        Name: "Jack",
        Age:  22,
        Addr: "China",
    }

    data2, err := json.Marshal(p2)
    if err != nil {
        panic(err)
    }

    // p2 则会打印所有
    fmt.Printf("%s\n", data2)
}

因此运行后,输出的结果如下

$ go run demo.go
{"name":"Jack","age":22}
{"name":"Jack","age":22,"addr":"China"}

3.2 不懂就问:如何定义获取 Tag ?

Tag 由反引号包含,由一对或几对的键值对组成,通过空格来分割键值。格式如下

`key01:"value01" key02:"value02" key03:"value03"`

定义完后,如何从结构体中,取出 Tag 呢?
答案就是,我们上一节学过的 “反射”。
获取 Tag 可以分为三个步骤:

  1. 获取字段 field
  2. 获取标签 tag
  3. 获取键值对 key:value

演示如下

// 三种获取 field
field := reflect.TypeOf(obj).FieldByName("Name")
field := reflect.ValueOf(obj).Type().Field(i)  // i 表示第几个字段
field := reflect.ValueOf(&obj).Elem().Type().Field(i)  // i 表示第几个字段

// 获取 Tag
tag := field.Tag

// 获取键值对
labelValue := tag.Get("label")
labelValue,ok := tag.Lookup("label")

获取键值对,有Get 和 Lookup 两种方法,但其实 Get 只是对 Lookup 函数的简单封装而已,当没有获取到对应 tag 的内容,会返回空字符串。

func (tag StructTag) Get(key string) string {
    v, _ := tag.Lookup(key)
    return v
}

空 Tag 和不设置 Tag 效果是一样的

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string ``
    Age string
}
func main() {
    p := reflect.TypeOf(Person{})
    name, _ := p.FieldByName("Name")
    fmt.Printf("%q\n", name.Tag) //输出 ""
    age, _ := p.FieldByName("Age")
    fmt.Printf("%q\n", age.Tag) // 输出 ""
}

3.3 实战一下:利用 Tag 搞点事情?

学会了如何定义 tag 和 获取 tag,可以试着利用 tag 来做一些事情,来练习一下。
这边我举个例子吧。
如果我想实现一个函数(就叫 Print 吧),在打印 person 对象时,能够美化输出

type Person struct {
    Name        string
    Age         int
    Gender      string
}

person := Person{
    Name:        "MING",
    Age:         29,
}

Print(person)

就像下面这样,key 和 value 之间有个 is:,如果没有指定 Gender 的值,那么显示为unknown(未知)。

Name is: MING
Age is: 29
Gender is: unknown

那该怎么做呢?
先改造下 Person 结构体,给每个字段加上 tag 标签,三个字段的tag 都有 label 属性,而 Gender 多了一个 default 属性,意在指定默认值。

type Person struct {
    Name        string `label:"Name is: "`
    Age         int    `label:"Age is: "`
    Gender      string `label:"Gender is: " default:"unknown"`
}

然后来写一下这个 Print 函数

func Print(obj interface{}) error {
    // 取 Value
    v := reflect.ValueOf(obj)

    // 解析字段
    for i := 0; i < v.NumField(); i++ {

        // 取tag
        field := v.Type().Field(i)
        tag := field.Tag

        // 解析label 和 default
        label := tag.Get("label")
        defaultValue := tag.Get("default")

        value := fmt.Sprintf("%v", v.Field(i))
        if value == "" {
            // 如果没有指定值,则用默认值替代
            value = defaultValue
        }

        fmt.Println(label + value)
    }

    return nil
}

最后执行一下,看了下输出,符合我们的预期:

$ go run demo.go
Name is: MING
Age is: 29
Gender is: unknown

到此,我们就把 Tag 的用法介绍完了。

4 学习接口:详解类型断言

Type Assertion

Type Assertion(中文名叫:类型断言),通过它可以做到以下几件事情

  1. 检查 i 是否为 nil
  2. 检查 i 存储的值是否为某个类型

具体的使用方式有两种:
第一种:

t := i.(T)

这个表达式可以断言一个接口对象(i)里不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回值给 t,如果断言失败,就会触发 panic。
来写段代码试验一下

package main

import "fmt"

func main() {
    var i interface{} = 10
    t1 := i.(int)
    fmt.Println(t1)

    fmt.Println("=====分隔线=====")

    t2 := i.(string)
    fmt.Println(t2)
}

运行后输出如下,可以发现在执行第二次断言的时候失败了,并且触发了 panic

10
=====分隔线=====
panic: interface conversion: interface {} is int, not string

goroutine 1 [running]:
main.main()
        E:/GoPlayer/src/main.go:12 +0x10e
exit status 2

如果要断言的接口值是 nil,那我们来看看也是不是也如预期一样会触发panic

package main

func main() {
    var i interface{} // nil
    var _ = i.(interface{})
}

输出如下,确实是会 触发 panic

panic: interface conversion: interface is nil, not interface {}

goroutine 1 [running]:
main.main()
        E:/GoPlayer/src/main.go:5 +0x34
exit status 2

第二种

t, ok:= i.(T)

和上面一样,这个表达式也是可以断言一个接口对象(i)里不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回其值给 t,并且此时 ok 的值 为 true,表示断言成功。
如果接口值的类型,并不是我们所断言的 T,就会断言失败,但和第一种表达式不同的事,这个不会触发 panic,而是将 ok 的值设为 false ,表示断言失败,此时t 为 T 的零值。
稍微修改下上面的例子,如下

package main

import "fmt"

func main() {
    var i interface{} = 10
    t1, ok := i.(int)
    fmt.Printf("%d-%t\n", t1, ok)

    fmt.Println("=====分隔线1=====")

    t2, ok := i.(string)
    fmt.Printf("%s-%t\n", t2, ok)

    fmt.Println("=====分隔线2=====")

    var k interface{} // nil
    t3, ok := k.(interface{})
    fmt.Println(t3, "-", ok)

    fmt.Println("=====分隔线3=====")
    k = 10
    t4, ok := k.(interface{})
    fmt.Printf("%d-%t\n", t4, ok)

    t5, ok := k.(int)
    fmt.Printf("%d-%t\n", t5, ok)
}

运行后输出如下,可以发现在执行第二次断言的时候,虽然失败了,但并没有触发了 panic。

10-true
=====分隔线1=====
-false
=====分隔线2=====
<nil> - false
=====分隔线3=====
10-true
10-true

上面这段输出,你要注意的是第二个断言的输出在-false 之前并不是有没有输出任何 t2 的值,而是由于断言失败,所以 t2 得到的是 string 的零值也是 "" ,它是零长度的,所以你看不到其输出。

Type Switch

如果需要区分多种类型,可以使用 type switch 断言,这个将会比一个一个进行类型断言更简单、直接、高效。

package main

import "fmt"

func findType(i interface{}) {
    switch x := i.(type) {
    case int:
        fmt.Println(x, "is int")
    case string:
        fmt.Println(x, "is string")
    case nil:
        fmt.Println(x, "is nil")
    default:
        fmt.Println(x, "not type matched")
    }
}

func main() {
    findType(10)      // int
    findType("hello") // string

    var k interface{} // nil
    findType(k)

    findType(10.23) //float64
}

输出如下

10 is int
hello is string
<nil> is nil
10.23 not type matched

额外说明一下:

  • 如果你的值是 nil,那么匹配的是 case nil
  • 如果你的值在 switch-case 里并没有匹配对应的类型,那么走的是 default 分支

此外,还有两点需要你格外注意

  1. 类型断言,仅能对静态类型为空接口(interface{})的对象进行断言,否则会抛出错误,具体内容可以参考:关于接口的三个”潜规则”
  2. 类型断言完成后,实际上会返回静态类型为你断言的类型的对象,而要清楚原来的静态类型为空接口类型(interface{}),这是 Go 的隐式转换。

5 学习接口:Go 语言中的空接口

5.1 什么是空接口?

空接口是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法口,也因此,我们可以说所有类型都至少实现了空接口。

type empty_iface interface {
}

每一个接口都包含两个属性,一个是值,一个是类型。
而对于空接口来说,这两者都是 nil,可以使用 fmt 来验证一下
输出如下

package main

import (
    "fmt"
)

func main() {
    var i interface{}
    fmt.Printf("type: %T, value: %v", i, i)
}
type: <nil>, value: <nil>

5.2 如何使用空接口?

第一,通常我们会直接使用 interface{} 作为类型声明一个实例,而这个实例可以承载任意类型的值。

package main

import (
    "fmt"
)

func main()  {
    // 声明一个空接口实例
    var i interface{}

    // 存 int 没有问题
    i = 1
    fmt.Println(i)

    // 存字符串也没有问题
    i = "hello"
    fmt.Println(i)

    // 存布尔值也没有问题
    i = false
    fmt.Println(i)
}

第二,如果想让你的函数可以接收任意类型的值 ,也可以使用空接口
接收一个任意类型的值 示例

package main

import (
    "fmt"
)

func myfunc(iface interface{}){
    fmt.Println(iface)
}

func main()  {
    a := 10
    b := "hello"
    c := true

    myfunc(a)
    myfunc(b)
    myfunc(c)
}

接收任意个任意类型的值 示例

package main

import (
    "fmt"
)

func myfunc(ifaces ...interface{}){
    for _,iface := range ifaces{
        fmt.Println(iface)
    }
}

func main()  {
    a := 10
    b := "hello"
    c := true

    myfunc(a, b, c)
}

第三,你也定义一个可以接收任意类型的 array、slice、map、strcut,例如这边定义一个切片

package main

import "fmt"

func main() {
    any := make([]interface{}, 5)
    any[0] = 11
    any[1] = "hello world"
    any[2] = []int{11, 22, 33, 44}
    for _, value := range any {
        fmt.Println(value)
    }
}

5.3 空接口几个要注意的坑

坑1:空接口可以承载任意值,但不代表任意类型就可以承接空接口类型的值
从实现的角度看,任何类型的值都满足空接口。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
但要是你把一个空接口类型的对象,再赋值给一个固定类型(比如 int, string等类型)的对象赋值,是会报错的。

package main

func main() {
    // 声明a变量, 类型int, 初始值为1
    var a int = 1

    // 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
    var i interface{} = a

    // 声明b变量, 尝试赋值i
    var b int = i
}

这个报错,它就好比可以放进行礼箱的东西,肯定能放到集装箱里,但是反过来,能放到集装箱的东西就不一定能放到行礼箱了,在 Go 里就直接禁止了这种反向操作。(声明:底层原理肯定还另有其因,但对于新手来说,这样解释也许会容易理解一些。)

.\main.go:11:6: cannot use i (type interface {}) as type int in assignment: need type assertion

坑2::当空接口承载数组和切片后,该对象无法再进行切片

package main

import "fmt"

func main() {
    sli := []int{2, 3, 5, 7, 11, 13}

    var i interface{}
    i = sli

    g := i[1:3]
    fmt.Println(g)
}

执行会报错。

.\main.go:11:8: cannot slice i (type interface {})

坑3:当你使用空接口来接收任意类型的参数时,它的静态类型是 interface{},但动态类型(是 int,string 还是其他类型)我们并不知道,因此需要使用类型断言。

package main

import (
    "fmt"
)

func myfunc(i interface{})  {

    switch i.(type) {
    case int:
        fmt.Println("参数的类型是 int")
    case string:
        fmt.Println("参数的类型是 string")
    }
}

func main() {
    a := 10
    b := "hello"
    myfunc(a)
    myfunc(b)
}

输出如下

参数的类型是 int
参数的类型是 string

6 学习接口:接口的三个”潜规则”

6.1 对方法的调用限制

接口是一组固定的方法集,由于静态类型的限制,接口变量有时仅能调用其中特定的一些方法。
请看下面这段代码

package main

import "fmt"

type Phone interface {
    call()
}

type iPhone struct {
    name string
}

func (phone iPhone)call()  {
    fmt.Println("Hello, iPhone.")
}

func (phone iPhone)send_wechat()  {
    fmt.Println("Hello, Wechat.")
}

func main() {
    var phone Phone
    phone = iPhone{name:"ming's iphone"}
    phone.call()
    phone.send_wechat()
}

我定义了一个 Phone 的接口,只要求实现 call 方法即可,也就是只要能打电话的设备就是一个电话(好像是一句没用的废话)。
然后再定义了一个 iPhone 的结构体,该结构体接收两个方法,一个是打电话( call 函数),一个是发微信(send_wechat 函数)。
最后一步是关键,我们定义了一个 Phone 接口类型的 phone 对象,该对象的内容是 iPhone 结构体。然后我们调用该对象的 call 方法,一切正常。
但是当你调用 phone.send_wechat方法,程序会报错,提示我们 Phone 类型的方法没有 send_wechat 的字段或方法。

# command-line-arguments
./demo.go:30:10: phone.send_wechat undefined (type Phone has no field or method send_wechat)

原因也很明显,因为我们的phone对象显式声明为 Phone 接口类型,因此 phone调用的方法会受到此接口的限制。
那么如何让 phone 可以调用 send_wechat 方法呢?
答案是可以不显式的声明为 Phone接口类型 ,但要清楚 phone 对象实际上是隐式的实现了 Phone 接口,如此一来,方法的调用就不会受到接口类型的约束。
修改 main 方法成如下

func main() {
    phone := iPhone{name:"ming's iphone"}
    phone.call()
    phone.send_wechat()
}

运行后,一切正常,没有报错。

Hello, iPhone.
Hello, Wechat.

6.2 调用函数时的隐式转换

Go 语言中的函数调用都是值传递的,变量会在方法调用前进行类型转换。
比如下面这段代码

import (
    "fmt"
)

func printType(i interface{})  {

    switch i.(type) {
    case int:
        fmt.Println("参数的类型是 int")
    case string:
        fmt.Println("参数的类型是 string")
    }
}

func main() {
    a := 10
    printType(a)
}

如果你运行后,会发现一切都很正常

参数的类型是 int

但是如果你把函数内的内容搬到到外面来

package main

import "fmt"


func main() {
    a := 10

    switch a.(type) {
    case int:
        fmt.Println("参数的类型是 int")
    case string:
        fmt.Println("参数的类型是 string")
    }
}

就会有意想不到的结果,居然报错了。

# command-line-arguments
./demo.go:9:5: cannot type switch on non-interface value a (type int)

这个操作会让一个新人摸不着头脑,代码逻辑都是一样的,为什么一个不会报错,一个会报错呢?
原因其实很简单。
当一个函数接口 interface{} 空接口类型时,我们说它可以接收什么任意类型的参数(江湖上称之为无招胜有招)。
当你使用这种写法时,Go 会默默地为我们做一件事,就是把传入函数的参数值(注意:Go 语言中的函数调用都是值传递的)的类型隐式的转换成 interface{} 类型。

如何进行接口类型的显式转换

上面了解了函数中 接口类型的隐式转换后,你的心里可能开始有了疑问了,难道我使用类型断言,只能通过一个接收空接口类型的函数才能实现吗?
答案当然是 No.
如果你想手动对其进行类型转换,可以像下面这样子,就可以将变量 a 的静态类型转换为 interface{} 类型然后赋值给 b (此时 a 的静态类型还是 int,而 b 的静态类型为 interface{})

var a int = 25
b := interface{}(a)

知道了方法后,将代码修改成如下:

package main

import "fmt"


func main() {
    a := 10

    switch interface{}(a).(type) {
    case int:
        fmt.Println("参数的类型是 int")
    case string:
        fmt.Println("参数的类型是 string")
    }
}

运行后,一切正常。

参数的类型是 int

6.3 类型断言中的隐式转换

上面我们知道了,只有静态类型为接口类型的对象才可以进行类型断言。
而当类型断言完成后,会返回一个静态类型为你断言的类型的对象,也就是说,当我们使用了类型断言,Go 实际上又会默认为我们进行了一次隐式的类型转换。
验证方法也很简单,使用完一次类型断言后,对返回的对象再一次使用类型断言,Goland 立马就会提示我们新对象 b 不是一个接口类型的对象,不允许进行类型断言。
image.png

7 学习反射:反射三定律

7.1 真实世界与反射世界

在本篇文章里,为了区分反射前后的变量值类型,我将反射前环境称为 真实世界,而将反射后的环境称为 反射世界。这种比喻不严谨,但是对于我理解是有帮助的,也希望对你有用。
在反射的世界里,我们拥有了获取一个对象的类型,属性及方法的能力。image.png

7.2 两种类型:Type 和 Value

在 Go 反射的世界里,有两种类型非常重要,是整个反射的核心,在学习 reflect 包的使用时,先得学习下这两种类型:

  1. reflect.Type
  2. reflect.Value

它们分别对应着真实世界里的 type 和 value,只不过在反射对象里,它们拥有更多的内容。
从源码上来看,reflect.Type 是以一个接口的形式存在的

type Type interface {
    Align() int
    FieldAlign() int
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int
    Name() string
    PkgPath() string
    Size() uintptr
    String() string
    Kind() Kind
    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    Bits() int
    ChanDir() ChanDir
    IsVariadic() bool
    Elem() Type
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)
    In(i int) Type
    Key() Type
    Len() int
    NumField() int
    NumIn() int
    NumOut() int
    Out(i int) Type
    common() *rtype
    uncommon() *uncommonType
}

而 reflect.Value 是以一个结构体的形式存在,

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

同时它接收了很多的方法(见下表),这里出于篇幅的限制这里也没办法一一介绍。

Addr
Bool
Bytes
runes
CanAddr
CanSet
Call
CallSlice
call
Cap
Close
Complex
Elem
Field
FieldByIndex
FieldByName
FieldByNameFunc
Float
Index
Int
CanInterface
Interface
InterfaceData
IsNil
IsValid
IsZero
Kind
Len
MapIndex
MapKeys
MapRange
Method
NumMethod
MethodByName
NumField
OverflowComplex
OverflowFloat
OverflowInt
OverflowUint
Pointer
Recv
recv
Send
send
Set
SetBool
SetBytes
setRunes
SetComplex
SetFloat
SetInt
SetLen
SetCap
SetMapIndex
SetUint
SetPointer
SetString
Slice
Slice3
String
TryRecv
TrySend
Type
Uint
UnsafeAddr
assignTo
Convert

通过上一节的内容(关于接口的三个 『潜规则』),我们知道了一个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。也就是说在真实世界里,type 和 value 是合并在一起组成 接口变量的。
而在反射的世界里,type 和 data 却是分开的,他们分别由 reflect.Type 和 reflect.Value 来表现。

7.3 解读反射的三大定律

Go 语言里有个反射三定律,是你在学习反射时,很重要的参考:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

翻译一下,就是:

  1. 反射可以将接口类型变量 转换为“反射类型对象”;
  2. 反射可以将 “反射类型对象”转换为 接口类型变量;
  3. 如果要修改 “反射类型对象” 其类型必须是 可写的;

第一定律

Reflection goes from interface value to reflection object.
为了实现从接口变量到反射对象的转换,需要提到 reflect 包里很重要的两个方法:

  1. reflect.TypeOf(i) :获得接口值的类型
  2. reflect.ValueOf(i):获得接口值的值

这两个方法返回的对象,我们称之为反射对象:Type object 和 Value object。image.png
举个例子,看下这两个方法是如何使用的?

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始接口变量的类型为 %T,值为 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 从接口变量到反射对象
    fmt.Printf("从接口变量到反射对象:Type对象的类型为 %T \n", t)
    fmt.Printf("从接口变量到反射对象:Value对象的类型为 %T \n", v)

}

输出如下

原始接口变量的类型为 int,值为 25
从接口变量到反射对象:Type对象的类型为 *reflect.rtype
从接口变量到反射对象:Value对象的类型为 reflect.Value

如此我们完成了从接口类型变量到反射对象的转换。
等等,上面我们定义的 age 不是 int 类型的吗?第一法则里怎么会说是接口类型的呢?
关于这点,其实在上一节(关于接口的三个 『潜规则』)已经提到过了,由于 TypeOf 和 ValueOf 两个函数接收的是 interface{} 空接口类型,而 Go 语言函数都是值传递,因此Go语言会将我们的类型隐式地转换成接口类型。

// TypeOf returns the reflection Type of the value in the interface{}.TypeOf returns nil.
func TypeOf(i interface{}) Type

// ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value

第二定律

Reflection goes from reflection object to interface value.
和第一定律刚好相反,第二定律描述的是,从反射对象到接口变量的转换。image.png
通过源码可知, reflect.Value 的结构体会接收 Interface 方法,返回了一个 interface{} 类型的变量(注意:只有 Value 才能逆向转换,而 Type 则不行,这也很容易理解,如果 Type 能逆向,那么逆向成什么呢?

// Interface returns v's current value as an interface{}.
// It is equivalent to:
//  var i interface{} = (v's underlying value)
// It panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) Interface() (i interface{}) {
    return valueInterface(v, true)
}

这个函数就是我们用来实现将反射对象转换成接口变量的一个桥梁。
例子如下

package main

import (
"fmt"
"reflect"
)

func main() {
    var age interface{} = 25

    fmt.Printf("原始接口变量的类型为 %T,值为 %v \n", age, age)

    t := reflect.TypeOf(age)
    v := reflect.ValueOf(age)

    // 从接口变量到反射对象
    fmt.Printf("从接口变量到反射对象:Type对象的类型为 %T \n", t)
    fmt.Printf("从接口变量到反射对象:Value对象的类型为 %T \n", v)

    // 从反射对象到接口变量
    i := v.Interface()
    fmt.Printf("从反射对象到接口变量:新对象的类型为 %T 值为 %v \n", i, i)

}

输出如下

原始接口变量的类型为 int,值为 25
从接口变量到反射对象:Type对象的类型为 *reflect.rtype
从接口变量到反射对象:Value对象的类型为 reflect.Value
从反射对象到接口变量:新对象的类型为 int 值为 25

当然了,最后转换后的对象,静态类型为 interface{} ,如果要转成最初的原始类型,需要再类型断言转换一下,关于这点,我已经在上一节里讲解过了,你可以点此前往复习:(关于接口的三个 『潜规则』)。

i := v.Interface().(int)

至此,我们已经学习了反射的两大定律,对这两个定律的理解,我画了一张图,你可以用下面这张图来加强理解,方便记忆。image.png

第三定律

To modify a reflection object, the value must be settable.
反射世界是真实世界的一个『映射』,是我的一个描述,但这并不严格,因为并不是你在反射世界里所做的事情都会还原到真实世界里。
第三定律引出了一个 settable (可设置性,或可写性)的概念。
其实早在以前的文章中,我们就一直在说,Go 语言里的函数都是值传递,只要你传递的不是变量的指针,你在函数内部对变量的修改是不会影响到原始的变量的。
回到反射上来,当你使用 reflect.Typeof 和 reflect.Valueof 的时候,如果传递的不是接口变量的指针,反射世界里的变量值始终将只是真实世界里的一个拷贝,你对该反射对象进行修改,并不能反映到真实世界里。
因此在反射的规则里

  • 不是接收变量指针创建的反射对象,是不具备『可写性』的
  • 是否具备『可写性』,可使用 CanSet() 来获取得知
  • 对不具备『可写性』的对象进行修改,是没有意义的,也认为是不合法的,因此会报错。
package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go"

    v := reflect.ValueOf(name)
    fmt.Println("可写性为:", v.CanSet())
}

输出如下

可写性为: false

要让反射对象具备可写性,需要注意两点

  1. 创建反射对象时传入变量的指针
  2. 使用 Elem()函数返回指针指向的数据

完整代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go"
    v1 := reflect.ValueOf(&name)
    fmt.Println("v1 可写性为:", v1.CanSet())

    v2 := v1.Elem()
    fmt.Println("v2 可写性为:", v2.CanSet())
}

输出如下

v1 可写性为: false
v2 可写性为: true

知道了如何使反射的世界里的对象具有可写性后,接下来是时候了解一下如何对修改更新它。
反射对象,都会有如下几个以 Set 单词开头的方法
image.png
这些方法就是我们修改值的入口。
来举个例子

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Go"
    fmt.Println("真实世界里 name 的原始值为:", name)

    v1 := reflect.ValueOf(&name)
    v2 := v1.Elem()

    v2.SetString("Python")
    fmt.Println("通过反射对象进行更新后,真实世界里 name 变为:", name)
}

输出如下

真实世界里 name 的原始值为: Go
通过反射对象进行更新后,真实世界里 name 变为: Python

8 学习反射:全面学习反射的函数

8.1 获取类别:Kind()

Type 对象 和 Value 对象都可以通过 Kind() 方法返回对应的接口变量的基础类型。

reflect.TypeOf(m).Kind()
reflect.ValueOf(m).Kind()

在这里,要注意的是,Kind 和 Type 是有区别的,Kind 表示更基础,范围更广的分类。
有一个例子来表示, iPhone (接口变量)的 Type 是手机,Kind 是电子产品。
通过查看源码文件: src/reflect/type.go ,可以得知 Kind 表示的基本都是 Go 原生的基本类型(共有 25 种的合法类型),而不包含自定义类型。

type Kind uint

const (
  Invalid Kind = iota  // 非法类型
  Bool                 // 布尔型
  Int                  // 有符号整型
  Int8                 // 有符号8位整型
  Int16                // 有符号16位整型
  Int32                // 有符号32位整型
  Int64                // 有符号64位整型
  Uint                 // 无符号整型
  Uint8                // 无符号8位整型
  Uint16               // 无符号16位整型
  Uint32               // 无符号32位整型
  Uint64               // 无符号64位整型
  Uintptr              // 指针
  Float32              // 单精度浮点数
  Float64              // 双精度浮点数
  Complex64            // 64位复数类型
  Complex128           // 128位复数类型
  Array                // 数组
  Chan                 // 通道
  Func                 // 函数
  Interface            // 接口
  Map                  // 映射
  Ptr                  // 指针
  Slice                // 切片
  String               // 字符串
  Struct               // 结构体
  UnsafePointer        // 底层指针
)

下面来看一下 Kind 函数如何使用?
第一种:传入值

package main

import (
    "fmt"
    "reflect"
)

type Profile struct {
    name string
    age int
    gender string
}

func main() {
    m := Profile{}

    t := reflect.TypeOf(m)
    fmt.Println("Type: ",t)
    fmt.Println("Kind: ",t.Kind())
}

输出如下

Type:  main.Profile
Kind:  struct

第二种:传入指针,关于 Elem() 的使用上一篇文章已经讲过了,它会返回 Type 对象所表示的指针指向的数据。

package main

import (
    "fmt"
    "reflect"
)

type Profile struct {
    name string
    age int
    gender string
}

func main() {
    m := Profile{}

    t := reflect.TypeOf(&m)

    fmt.Println("&m Type: ",t)
    fmt.Println("&m Kind: ",t.Kind())

    fmt.Println("m Type: ",t.Elem())
    fmt.Println("m Kind: ",t.Elem().Kind())
}

输出如下

&m Type:  *main.Profile
&m Kind:  ptr
m Type:  main.Profile
m Kind:  struct

如果这里不使用 TypeOf 方法,而是使用 ValueOf 方法呢,应该这样子写

package main

import (
    "fmt"
    "reflect"
)

type Profile struct {
    name string
    age int
    gender string
}

func main() {
    m := Profile{}

    v := reflect.ValueOf(&m)

    fmt.Println("&m Type: ",v.Type())
    fmt.Println("&m Kind: ",v.Kind())

    fmt.Println("m Type: ",v.Elem().Type())
    fmt.Println("m Kind: ",v.Elem().Kind())
}

8.2 进行类型的转换

Int() :转成 int

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var age int = 25

    v1 := reflect.ValueOf(age)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.Int()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: 25
转换后, type: int64, value: 25

Float():转成 float

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var score float64 = 99.5

    v1 := reflect.ValueOf(score)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.Float()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: 99.5
转换后, type: float64, value: 99.5

String():转成 string

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var name string = "Go"

    v1 := reflect.ValueOf(name)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.String()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: Go
转换后, type: string, value: Go

Bool():转成布尔值

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var isMale bool = true

    v1 := reflect.ValueOf(isMale)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.Bool()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: true
转换后, type: bool, value: true

Pointer():转成指针

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var age int = 25

    v1 := reflect.ValueOf(&age)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.Pointer()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: 0xc0000b4008
转换后, type: uintptr, value: 824634458120

Interface():转成接口类型

由于空接口类型可以接收任意类型的值,所以上面介绍的各种方法,其实都可以用 Interface() 函数来代替。
区别只有一个,使用 Interface() 返回的对象,静态类型为 interface{},而使用 Int ()、String() 等函数,返回的对象,其静态类型会是 int,string 等更具体的类型。
关于 Interface() 示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var age int = 25

    v1 := reflect.ValueOf(age)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)
    v2 := v1.Interface()
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: 25
转换后, type: int, value: 25

8.3 对切片的操作

Slice():对切片再切片(两下标)

Slice() 函数与上面所有类型转换的函数都不一样,它返回还是 reflect.Value 反射对象,而不再是我们所想的真实世界里的切片对象。
通过以下示例代码可验证

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var numList []int = []int{1,2}

    v1 := reflect.ValueOf(numList)
    fmt.Printf("转换前, type: %T, value: %v \n", v1, v1)

    // Slice 函数接收两个参数
    v2 := v1.Slice(0, 2)
    fmt.Printf("转换后, type: %T, value: %v \n", v2, v2)
}

输出如下

转换前, type: reflect.Value, value: [1 2]
转换后, type: reflect.Value, value: [1 2]

Slice3():对切片再切片(三下标)

Slice3() 与 Slice() 函数一样,都是对一个切片的反射对象

Set() 和 Append():更新切片

示例代码如下

package main

import (
    "fmt"
    "reflect"
)

func appendToSlice(arrPtr interface{}) {
    valuePtr := reflect.ValueOf(arrPtr)
    value := valuePtr.Elem()

    value.Set(reflect.Append(value, reflect.ValueOf(3)))

    fmt.Println(value)
    fmt.Println(value.Len())
}

func main() {
    arr := []int{1,2}

    appendToSlice(&arr)

    fmt.Println(arr)
}

输出如下

3
[1 2 3]
[1 2 3]

8.4 对属性的操作

NumField() 和 Field()

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    name string
    age int
    gender string
}

func (p Person)SayBye()  {
    fmt.Println("Bye")
}

func (p Person)SayHello()  {
    fmt.Println("Hello")
}



func main() {
    p := Person{"写代码的明哥", 27, "male"}

    v := reflect.ValueOf(p)

    fmt.Println("字段数:", v.NumField())
    fmt.Println("第 1 个字段:", v.Field(0))
    fmt.Println("第 2 个字段:", v.Field(1))
    fmt.Println("第 3 个字段:", v.Field(2))

    fmt.Println("==========================")
    // 也可以这样来遍历
    for i:=0;i<v.NumField();i++{
        fmt.Printf("第 %d 个字段:%v \n", i+1, v.Field(i))
    }
}

输出如下

字段数: 3
第 1 个字段: 写代码的明哥
第 2 个字段: 27
第 3 个字段: male
==========================
第 1 个字段:写代码的明哥
第 2 个字段:27
第 3 个字段:male

8.5 对方法的操作

NumMethod() 和 Method()

要获取 Name ,注意使用使用 TypeOf

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    name string
    age int
    gender string
}

func (p Person)SayBye()  {
    fmt.Println("Bye")
}

func (p Person)SayHello()  {
    fmt.Println("Hello")
}



func main() {
    p := &Person{"写代码的明哥", 27, "male"}

    t := reflect.TypeOf(p)

    fmt.Println("方法数(可导出的):", t.NumMethod())
    fmt.Println("第 1 个方法:", t.Method(0).Name)
    fmt.Println("第 2 个方法:", t.Method(1).Name)

    fmt.Println("==========================")
    // 也可以这样来遍历
    for i:=0;i<t.NumMethod();i++{
       fmt.Printf("第 %d 个方法:%v \n", i+1, t.Method(i).Name)
    }
}

输出如下

方法数(可导出的): 2
第 1 个方法: SayBye
第 2 个方法: SayHello
==========================
第 1 个方法:SayBye
第 2 个方法:SayHello

8.6 动态调用函数(使用索引且无参数)

要调用 Call,注意要使用 ValueOf

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    name string
    age int
}

func (p Person)SayBye() string {
    return "Bye"
}

func (p Person)SayHello() string {
    return "Hello"
}


func main() {
    p := &Person{"wangbm", 27}

    t := reflect.TypeOf(p)
    v := reflect.ValueOf(p)


    for i:=0;i<v.NumMethod();i++{
       fmt.Printf("调用第 %d 个方法:%v ,调用结果:%v\n",
           i+1,
           t.Method(i).Name,
           v.Elem().Method(i).Call(nil))
    }
}

输出如下

调用第 1 个方法:SayBye ,调用结果:[Bye]
调用第 2 个方法:SayHello ,调用结果:[Hello]

8.7 动态调用函数(使用函数名且无参数)

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    name string
    age int
    gender string
}

func (p Person)SayBye()  {
    fmt.Print("Bye")
}

func (p Person)SayHello()  {
    fmt.Println("Hello")
}



func main() {
    p := &Person{"写代码的明哥", 27, "male"}

    v := reflect.ValueOf(p)

    v.MethodByName("SayHello").Call(nil)
    v.MethodByName("SayBye").Call(nil)
}

8.8 动态调用函数(使用函数且有参数)

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
}

func (p Person)SelfIntroduction(name string, age int)  {
    fmt.Printf("Hello, my name is %s and i'm %d years old.", name, age)
}



func main() {
    p := &Person{}

    //t := reflect.TypeOf(p)
    v := reflect.ValueOf(p)
    name := reflect.ValueOf("wangbm")
    age := reflect.ValueOf(27)
    input := []reflect.Value{name, age}
    v.MethodByName("SelfIntroduction").Call(input)
}

输出如下

Hello, my name is wangbm and i'm 27 years old.

8.9 如何看待反射?

反射 提供了一些在早期高级语言中难以实现的运行时特性

  • 可以在一定程度上避免硬编码,提供灵活性和通用性。
  • 可以作为一个第一类对象发现并修改源代码的结构(如代码块、类、方法、协议等)。
  • 可以在运行时像对待源代码语句一样动态解析字符串中可执行的代码(类似JavaScript的eval()函数),进而可将跟class或function匹配的字符串转换成class或function的调用或引用。
  • 可以创建一个新的语言字节码解释器来给编程结构一个新的意义或用途。

劣势

  • 此技术的学习成本高。面向反射的编程需要较多的高级知识,包括框架、关系映射和对象交互,以实现更通用的代码执行。
  • 同样因为反射的概念和语法都比较抽象,过多地滥用反射技术会使得代码难以被其他人读懂,不利于合作与交流。
  • 由于将部分信息检查工作从编译期推迟到了运行期,此举在提高了代码灵活性的同时,牺牲了一点点运行效率。

通过深入学习反射的特性和技巧,它的劣势可以尽量避免,但这需要许多时间和经验的积累。

几点说明

  1. 有 reflect 的代码一般都较难理解,使用时请注意适当。
  2. Golang 的反射很慢,这个和它的 API 设计有关
  3. 反射是一个高级知识点,内容很多,不容易掌握,应该小心谨慎的使用它
  4. 不到不得不用的地步,能避免使用反射就不用。

在开发中,你或许会碰到在有些情况下,你需要获取一个对象的类型,属性及方法,而这个过程其实就是 反射
通过反射你可以实现一些动态的功能,提高了 Go 作为一门静态语言的灵活性。
Go 原生为我们内置了一个 reflect 包来为对象提供反射能力,本篇文章将重点于这个 reflect 包的使用。

9 详细图解:静态类型与动态类型

9.1 静态类型

所谓的静态类型(即 static type),就是变量声明的时候的类型。

var age int   // int 是静态类型
var name string  // string 也是静态类型

它是你在编码时,肉眼可见的类型。

8.2 动态类型

所谓的 动态类型(即 concrete type,也叫具体类型)是 程序运行时系统才能看见的类型。
这是什么意思呢?
我们都知道 空接口 可以承接任意类型的值,什么 int 呀,string 呀,都可以接收。
比如下面这几行代码

var i interface{}

i = 18
i = "Go"

第一行:我们在给 i 声明了 interface{} 类型,所以 i 的静态类型就是 interface{}
第二行:当我们给变量 i 赋一个 int 类型的值时,它的静态类型还是 interface{},这是不会变的,但是它的动态类型此时变成了 int 类型。
第三行:当我们给变量 i 赋一个 string 类型的值时,它的静态类型还是 interface{},它还是不会变,但是它的动态类型此时又变成了 string 类型。
从以上,可以知道,不管是 i=18 ,还是 i="Go",都是当程序运行到这里时,变量的类型,才发生了改变,这就是我们最开始所说的 动态类型是程序运行时系统才能看见的类型。

9.3 接口组成

每个接口变量,实际上都是由一 pair 对(type 和 data)组合而成,pair 对中记录着实际变量的值和类型。
比如下面这条语句

var age int = 25

我们声明了一个 int 类型变量,变量名叫 age ,其值为 25
image.png
知道了接口的组成后,我们在定义一个变量时,除了使用常规的方法(可参考:02. 学习五种变量创建的方法
也可以使用像下面这样的方式

package main

import "fmt"

func main() {
    age := (int)(25)
    //或者使用 age := (interface{})(25)

    fmt.Printf("type: %T, data: %v ", age, age)
}

输出如下

type: int, data: 25

9.4 接口细分

根据接口是否包含方法,可以将接口分为 iface 和 eface。

iface

第一种:iface,表示带有一组方法的接口。
比如

type Phone interface {
   call()
}

iface 的具体结构可用如下一张图来表示
image.png
iface 的源码如下:

// runtime/runtime2.go
// 非空接口
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

// 非空接口的类型信息
type itab struct {
    inter  *interfacetype  // 接口定义的类型信息
    _type  *_type      // 接口实际指向值的类型信息
    link   *itab
    bad    int32
    inhash int32
    fun    [1]uintptr   // 接口方法实现列表,即函数地址列表,按字典序排序
}

// runtime/type.go
// 非空接口类型,接口定义,包路径等。
type interfacetype struct {
   typ     _type
   pkgpath name
   mhdr    []imethod      // 接口方法声明列表,按字典序排序
}
// 接口的方法声明
type imethod struct {
   name nameOff          // 方法名
   ityp typeOff                // 描述方法参数返回值等细节
}

eface

第二种:eface,表示不带有方法的接口
比如

var i interface{}

eface 的源码如下:

// src/runtime/runtime2.go
// 空接口
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

image.png

9.5 理解动态类型

前两节,我们知道了什么是动态类型?如何让一个对象具有动态类型?
后两节,我们知道了接口分两种,它们的内部结构各是什么样的?
那最后一节,可以将前面四节的内容结合起来,看看在给一个空接口类型的变量赋值时,接口的内部结构会发生怎样的变化 。

iface

先来看看 iface,有如下一段代码:

var reader io.Reader

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}

reader = tty

第一行代码:var reader io.Reader ,由于 io.Reader 接口包含 Read 方法,所以 io.Reader 是 iface,此时 reader 对象的静态类型是 io.Reader,暂无动态类型。
image.png
最后一行代码:reader = tty,tty 是一个 *os.File 类型的实例,此时reader 对象的静态类型还是 io.Reader,而动态类型变成了 *os.File。
image.png

eface

再来看看 eface,有如下一段代码:

//不带函数的interface
var empty interface{}

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}

empty = tty

第一行代码:var empty interface{},由于 interface{} 是一个 eface,其只有一个 _type 可以存放变量类型,此时 empty 对象的(静态)类型是 nil。
image.png
最后一行代码:empty = tty,tty 是一个 *os.File 类型的实例,此时 _type 变成了 *os.File。
image.png

9.6 反射的必要性

由于动态类型的存在,在一个函数中接收的参数的类型有可能无法预先知晓,此时我们就要对参数进行反射,然后根据不同的类型做不同的处理。

10 关键字:make 和 new 的区别?

10.1 new 函数

在官方文档中,new 函数的描述如下

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

可以看到,new 只能传递一个参数,该参数为一个任意类型,可以是Go语言内建的类型,也可以是你自定义的类型
那么 new 函数到底做了哪些事呢:

  • 分配内存
  • 设置零值
  • 返回指针(重要)

举个例子

import "fmt"

type Student struct {
   name string
   age int
}

func main() {
    // new 一个内建类型
    num := new(int)
    fmt.Println(*num) //打印零值:0

    // new 一个自定义类型
    s := new(Student)
    s.name = "wangbm"
}

10.2 make 函数

在官方文档中,make 函数的描述如下

//The make built-in function allocates and initializes an object
//of type slice, map, or chan (only). Like new, the first argument is
// a type, not a value. Unlike new, make's return type is the same as
// the type of its argument, not a pointer to it.

func make(t Type, size ...IntegerType) Type

翻译一下注释内容

  1. 内建函数 make 用来为 slice,map 或 chan 类型(注意:也只能用在这三种类型上)分配内存和初始化一个对象
  2. make 返回类型的本身而不是指针,而返回值也依赖于具体传入的类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

注意,因为这三种类型是引用类型,所以必须得初始化(size和cap),但不是置为零值,这个和new是不一样的。
举几个例子

//切片
a := make([]int, 2, 10)

// 字典
b := make(map[string]int)

// 通道
c := make(chan int, 10)

10.3 总结

new:为所有的类型分配内存,并初始化为零值,返回指针。
make:只能为 slice,map,chan 分配内存,并初始化,返回的是类型。
另外,目前来看 new 函数并不常用,大家更喜欢使用短语句声明的方式。

a := new(int)
*a = 1
// 等价于
a := 1

但是 make 就不一样了,它的地位无可替代,在使用slice、map以及channel的时候,还是要使用make进行初始化,然后才可以对他们进行操作。

11 面向对象:Go 语言中的空结构体

11.1 普通的理解

在结构体中,可以包裹一系列与对象相关的属性,但若该对象没有属性呢?那它就是一个空结构体。
空结构体,和正常的结构体一样,可以接收方法函数。

type Lamp struct{}

func (l Lamp) On() {
        println("On")

}
func (l Lamp) Off() {
        println("Off")
}

11.2 空结构体的妙用

空结构体的表象特征,就是没有任何属性,而从更深层次的角度来说,空结构体是一个不占用空间的对象。
使用 unsafe.Sizeof 可以轻易的验证这个结果

type Lamp struct{}

func main() {
    lamp := Lamp{}
    fmt.Print(unsafe.Sizeof(lamp))
}
// output: 0

而这个特性,与结构体有没有接收函数是没有关系的。

type Lamp struct{}

func (l Lamp) On ()  {
    fmt.Println("On...")
}

func main() {
    lamp := Lamp{}
    fmt.Print(unsafe.Sizeof(lamp))
}
// output: 0

基于这个特性,在一些特殊的场合之下,可以用做占位符使用,合理的使用空结构体,会减小程序的内存占用空间。
比如在使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。

func main() {
    ch := make(chan struct{}, 1)
    go func() {
        <-ch
        // do something
    }()
    ch <- struct{}{}
    // ...
}
posted @ 2024-03-14 23:21  liuyang9643  阅读(5)  评论(0编辑  收藏  举报