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

结构体是将多个容易类型的命令变量组合在一起的聚合数据类型,每个变量都成为该结构体的成员变量,golang中的struct结构体和其他语言中的class类似。
1、结构体声明

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

2、定义方法

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.属性名 的方法来访问实例属性。
实例:

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()
}
//output
名字:小明
年龄:24
性别:male

3、方法的参数传递
当我们想要在方法内改变实例的属性的时候,必须使用指针做为方法的接收者。

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)
}
//output
当前年龄:24
当前年龄:25

定义方法的两种方式:
(1)以值作为方法接收者
(2)以指针作为方法的接收者:你需要在方法内部改变结构体内容的时候或者出于性能的问题,当结构体过大的时候。一般情况下建议都使用指针作为接收者,但是不管使用哪种方法,指针实例对象、值实例对象都可以直接调用

4、结构体实现“继承”
golang本身并不支持继承,但是我们可以使用组合的方式实现继承的效果,一个结构体嵌入到另一个结构体中:

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)
}
//output
小明 在 Tencent 工作
小明 在 Tencent 工作

5、结构体内外部方法
函数的首字母大小非常重要!

(1)当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用

(2)当方法的首字母为小写时,这个方法是Private,其他包是无法访问的。

2、面向对象编程:接口和多态

2.1 接口

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

type Phone interface {
   call()
}

接口的实现:

如果有一个类型/结构体,实现了一个接口要求的所有方法,这里 Phone 接口只有 call方法,所以只要实现了 call 方法,我们就可以称它实现了 Phone 接口。

type Nokia struct {
    name string
}

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

接口实现多态:

在一个接口下,在不同对象上的不同表现,这就是多态,在golang中是通过接口实现的多态

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 (freeGit FreeGift) settleAccount() int  {
    return 0
}

func (freeGit FreeGift)  orderInfo() string  {
    return "您要购买" + strconv.Itoa(freeGit.quantity) + "个" + freeGit.name + "共消费:" + strconv.Itoa(freeGit.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:"earphone",
        quantity: 1,
        price: 200,
    }
    goods := []Good{iphone,earphones}
    allPrice := calculateAllPrice(goods)
    fmt.Printf("该订单总共需要支付%d元",allPrice)
}
//output
您要购买1个iPhone计:8000元
您要购买1个耳机计:0元
该订单总共需要支付 8000 元

3、关键字:make和new的区别

3.1 new

func new(Type) *Type

new函数只能传递一个参数,该参数为任意类型
new的主要工作:
1、分配内存
2、设置零值
3、返回指针

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"
}

3.2 make

1、内建函数 make 用来为 slice,map 或 chan 类型(注意:也只能用在这三种类型上)分配内存和初始化一个对象

2、make 返回类型的本身而不是指针,而返回值也依赖于具体传入的类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

Attention:因为这三种类型是引用类型,所以必须得初始化(size和cap),但是不是置为零值,这个和new是不一样的

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

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

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

总结
new:为所有的类型分配内存,并初始化为零值,返回指针。

make:只能为 slice,map,chan 分配内存,并初始化,返回的是类型。

4、语句块和作用域

1、显示语句块和隐式语句块

语句块是{}包含的一系列语句,一般用{}包含的语句块属于显示语句块

语句块内部声明的变量外部无法访问,这个块就是内部声明的变量的作用范围,也就是作用域。

golang中的隐式语句块:
(1)主语句块:包括所有源码,对应内置作用域

(2)包语句块:包括该包中所有的源码(一个包可能会包括一个目录下的多个文件),对应包级作用域

(3)文件语句块:包括该文件中的所有源码,对应文件级作用域

(4)for 、if、switch等语句本身也在它自身的隐式语句块中,对应局部作用域

2、作用域的理解

根据变量声明的位置不同,作用域可以分为四种类型:
(1)内置作用域:不需要自己声明,所有的关键字和内置类型、函数都拥有全局作用域

(2)包级作用域:必須函数外声明,在该包内的所有文件都可以访问

(3)文件级作用域:不需要声明,导入即可。一个文件中通过import导入的包名,只在该文件内可用

(4)局部作用域:在自己的语句块内声明,包括函数,for、if 等语句块,或自定义的 {} 语句块形成的作用域,只在自己的局部作用域内可用

以上的四种作用域,从上往下,范围从大到小,为了表述方便,我这里自己将范围大的作用域称为高层作用域,而范围小的称为低层作用域

对于作用域,有以下几点总结:
(1)低层作用域,可以访问高层作用域
(2)同一层级的作用域,是相互隔离的
(3)低层作用域里声明的变量,会覆盖高层作用域里声明的变量

在这里要注意一下,不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。

而一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念

3、静态作用域和动态作用域

图片说明
从代码中,可以看到在 func01 函数中定义了个局部变量 value,按理说,这个 value 变量只在该函数内可用,但由于在 shell 中的作用域是动态的,所以在 func01中也可以调用 func02 时,func02 可以访问到 value 变量,此时的 func02 作用域可以当成是 局部作用域中(func01)的局部作用域。但若脱离了 func01的执行环境,将其放在全局环境下或者其他函数中, func02 是访问不了 value 变量的。

但在 Go 中并不存在这种动态作用域,比如这段代码,在func01函数中,要想取得 name 这个变量,只能从func01的作用域或者更高层作用域里查找(文件级作用域,包级作用域和内置作用域),而不能从调用它的另一个局部作用域中(因为他们在层级上属于同一级)查找。

5、空接口

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

type empty_iface interface {
}

每一个接口都包含两个属性,一个是值,一个是类型,而对于空接口来说,这两者都是 nil.

package main

import (
    "fmt"
)

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

空接口的使用:
1、通常我们会直接使用 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)
}

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

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)
}

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

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)
}

3、定义一个可以接收任意类型的 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)
    }
}

注意事项
1、空接口可以承载任意值,但不代表任意类型就可以承接空接口类型的值,把一个空接口类型的对象,再赋值给一个固定类型(比如 int, string等类型)的对象赋值,是会报错的

2、当空接口承载数组和切片后,该对象无法再进行切片
图片说明

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)
}
//output
参数的类型是 int
参数的类型是 string

6、静态类型与动态类型

静态类型
static type,就是变量声明时的类型

var age int
var name string

动态类型
concrete type,也叫具体类型,是程序运行时系统才能看见的类型

var i interface{}

i = 18
i = "Go编程时光"

第一行:给 i 声明了 interface{} 类型,所以 i 的静态类型就是 interface{}

第二行:当我们给变量 i 赋一个 int 类型的值时,它的静态类型还是 interface{},这是不会变的,但是它的动态类型此时变成了 int 类型。

第三行:当我们给变量 i 赋一个 string 类型的值时,它的静态类型还是 interface{},但是它的动态类型此时又变成了 string 类型。

由此可知,不管是 i=18 ,还是 i="Go编程时光",都是当程序运行到这里时,变量的类型,才发生了改变,这就是我们最开始所说的 动态类型是程序运行时系统才能看见的类型。

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

var age int = 25

图片说明
因此在定义变量时,除了常规的方法,还可以使用下面的方法:

package main

import "fmt"

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

    fmt.Printf("type: %T, data: %v ", age, age)
}
//output
type:int,data:25

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

1、iface,表示带有一组方法的接口

type Phone interface {
   call()
}

图片说明
图片说明

2、eface,表示不带方法的接口

var i interface{}

图片说明

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

理解动态类型
在给空接口类型的变量赋值时,接口的内部结构会发生什么变化
图片说明
第一行代码:var reader io.Reader ,由于 io.Reader 接口包含 Read 方法,所以 io.Reader 是 iface,此时 reader 对象的静态类型是 io.Reader,暂无动态类型。
图片说明
最后一行代码:reader = tty,tty 是一个 *os.File 类型的实例,此时reader 对象的静态类型还是 io.Reader,而动态类型变成了 *os.File。
图片说明

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

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

7、关于接口的三个“潜规则”

1、对方法的调用限制
因为接口是一组固定的方法集,由于静态类型的限制,接口变量有时候只能调用其中特定的一些方法

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.send_wechat()这里会报错,因为Phone接口里面并没有声明send_wechat字段和方法,因为phone被显式声明为Phone接口类型,受到该接口的限制。
可以将声明变为隐式声明

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

这样就不会报错了
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)
}
//output
参数类型是int

但是如果将printType函数内的内容放到main函数里面,即:

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{} 类型。

那么如何进行接口类型的显式转换呢?

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")
    }
}
//output
参数的类型是 int

3.类型断言中的隐式转换

只有静态类型为接口类型的对象才可以进行类型断言

而当类型断言完成后,会返回一个静态类型为你断言的类型的对象,也就是说,当我们使用了类型断言,Go 实际上又会默认为我们进行了一次隐式的类型转换。
图片说明
用完一次类型断言后,对返回的对象再一次使用类型断言,Goland 立马就会提示我们新对象 b 不是一个接口类型的对象,不允许进行类型断言

8、反射三定律

由于动态类型的存在,在一个函数中接收的参数的类型可能无法预先知晓,此事我们需要对参数进行反射,然后根据不用的类型做不同的处理
图片说明
Type 和 Value
在go的反射世界里,Type和Value是整个反射的核心,它们分别对应真实世界的type和value:

reflect.Type
reflect.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

真实世界里,type 和 value 是合并在一起组成 接口变量的。而在反射的世界里,type 和 data 却是分开的,他们分别由 reflect.Type 和 reflect.Value 来表现

反射三大定律
1、第一定律:反射可以将接口类型变量转换为“反射类型对象”

实现从接口变量到反射对象的转换:

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

图片说明

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)

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

两个函数接收的是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

2、第二定律:从反射对象转换到接口变量
图片说明
通过源码可知, reflect.Value 的结构体会接收 Interface 方法,返回了一个 interface{} 类型的变量(注意:只有 Value 才能逆向转换,而 Type 则不行)

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)

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

最后转换后的对象,静态类型为 interface{} ,如果要转成最初的原始类型,需要再类型断言转换一下

对于前两个定律的理解可以如下图:
图片说明

3、第三定律:如果要修改 “反射类型对象” 其类型必须是 可写的
反射世界是真实世界的一个『映射』,只是一个描述,这并不严格,因为并不是你在反射世界里所做的事情都会还原到真实世界里

Go 语言里的函数都是值传递,只要你传递的不是变量的指针,你在函数内部对变量的修改是不会影响到原始的变量的。回到反射上来,当你使用 reflect.Typeof 和 reflect.Valueof 的时候,如果传递的不是接口变量的指针,反射世界里的变量值始终将只是真实世界里的一个拷贝,你对该反射对象进行修改,并不能反映到真实世界里

反射规则:

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

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

  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())
}
//output
v1 可写性为: false
v2 可写性为: true

对值进行更新的方法:
图片说明

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)
}
//output
真实世界里 name 的原始值为: Go编程时光
通过反射对象进行更新后,真实世界里 name 变为: Python编程时光

9、深入学习反射的应用

1、Kind()函数--获取类别
Type 对象 和 Value 对象都可以通过 Kind() 方法返回对应的接口变量的基础类型

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

Kind 和 Type 是有区别的,Kind 表示更基础,范围更广的分类:例如iPhone (接口变量)的 Type 是手机,Kind 是电子产品
Kind 表示的基本都是 Go 原生的基本类型(共有 25 种的合法类型),而不包含自定义类型
图片说明

Kind函数的使用:
1、传入值

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())
}
//output
Type:  main.Profile
Kind:  struct

2、传入指针

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())
}
//output
&m Type:  *main.Profile
&m Kind:  ptr
m Type:  main.Profile
m Kind:  struct

如果不使用TypeOf方法而使用ValueOf方法,直接定义v := reflect.ValueOf(&m),然后将其他部分的t换成v

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)
}
//output
转换前, type: reflect.Value, value: 25
转换后, type: int64, value: 25

Float():转成 float
String():转成 string
Bool():转成布尔值
Pointer():转成指针
Interface():转成接口类型。由于空接口类型可以接收任意类型的值,所以上面介绍的各种方法,其实都可以用 Interface() 函数来代替。
区别只有一个,使用 Interface() 返回的对象,静态类型为 interface{},而使用 Int ()、String() 等函数,返回的对象,其静态类型会是 int,string 等更具体的类型

3、对切片进行操作
Slice() 函数与上面所有类型转换的函数都不一样,它返回还是 reflect.Value 反射对象,而不再是我们所想的真实世界里的切片对象
Slice(i,j),接收两个参数,表示截取原切片[i,j),左闭右开

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)
}
//output
转换前, type: reflect.Value, value: [1 2]
转换后, type: reflect.Value, value: [1 2]

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

package main

import (
    "fmt"
    "reflect"
)

func appendToSlice(arrPtr interface{}) {
    //反射的第三定律
    valuePtr := reflect.ValueOf(arrPtr)
    value := valuePtr.Elem()
    //向里面添加新的元素3
    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)
}
//output
3
[1 2 3]
[1 2 3]

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))
    }
}
//output
字段数: 3
第 1 个字段: 写代码的明哥
第 2 个字段: 27
第 3 个字段: male
==========================
第 1 个字段:写代码的明哥
第 2 个字段:27
第 3 个字段:male

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)
    }
}
//output
方法数(可导出的): 2
第 1 个方法: SayBye
第 2 个方法: SayHello
==========================
第 1 个方法:SayBye
第 2 个方法:SayHello

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))
    }
}
//output
调用第 1 个方法:SayBye ,调用结果:[Bye]
调用第 2 个方法:SayHello ,调用结果:[Hello]

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、动态调用函数(使用函数且有参数)

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)
}
//output
Hello, my name is wangbm and i'm 27 years old.

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

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

劣势

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

说明

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

在开发中,你或许会碰到在有些情况下,你需要获取一个对象的类型,属性及方法,而这个过程其实就是 反射。

通过反射你可以实现一些动态的功能,提高了 Go 作为一门静态语言的灵活性。

10、 反引号的妙用:结构体里的 Tag 用法

什么是Tag?

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

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

一般情况下,结构体的定义中,每个字段都由名字和字段类型组成

type Person struct {
    Name string
    Age int
    Addr string
}

但是字段上还可以额外增加属性,使用反引号包含的字符串就是Tag,也就是标签

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

查看一下标签的效果:

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)
}
//output
{"name":"Jack","age":22}
{"name":"Jack","age":22,"addr":"China"}

由于 Person 结构体里的 Addr 字段有 omitempty 属性,因此 encoding/json 在将对象转化 json 字符串时,只要发现对象里的 Addr 为 false, 0, 空指针,空接口,空数组,空切片,空映射,空字符串中的一种,就会被忽略

怎么获取Tag?
图片说明

// 三种获取 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 的内容,会返回空字符串。

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) // 输出 ""
}
//output
""
""

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

3、tag的实用

package main

import (
    "fmt"
    "reflect"
)

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

func Print(obj interface{}) error  {
    //取value
    v := reflect.ValueOf(obj)
    //解析字段
    for i := 0 ; i < v.NumField() ; i++ {
        field := v.Type().Field(i)
        tag := field.Tag

        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
}
func main()  {
    person := Person{Name: "ming xiao",Age: 23,Gender: "girl"}
    Print(person)
}
//output
Name is: ming xiao
Age is: 23
Gender is: girl