专栏首页米奇爱编程Golang高效实践之interface、reflection、json实践

Golang高效实践之interface、reflection、json实践

前言

反射是程序校验自己数据结构和类型的一种机制。文章尝试解释Golang的反射机制工作原理,每种编程语言的反射模型都是不同的,有很多语言甚至都不支持反射。

Interface

在将反射之前需要先介绍下接口interface,因为Golang的反射实现是基于interface的。Golang是静态类型语言,每个变量拥有一个静态类型,在编译器就已经确定,例如int,float32,*MyType, []byte等等。如果我们定义:

type MyInt int 
var i int
var j MyInt

int类型的I和MyInt类型的j是不同类型的变量,在没有限制类型转换的情况下它们不能相互赋值,即便它们的底层类型是一样的。

接口interface类型是最重要的一种数据类型,代表的一些方法的集合。interface变量可以存储任意的数据类型,只要该数据类型实现了interface的方法集合。例如io包的io.Reader和io.Writer:

// Reader is the interface that wraps the basic Read method.

type Reader interface {

    Read(p []byte) (n int, err error)

}

// Writer is the interface that wraps the basic Write method.

type Writer interface {

    Write(p []byte) (n int, err error)

}

任意实现了Read方法的类型都是Reader类型,也就是说可以赋值给Reader接口,换句话说就是Reader interface可以存储任意的实现了Read方法的类型:

var r io.Reader

r = os.Stdin

r = bufio.NewReader(r)

r = new(bytes.Buffer)

// and so on

需要明确的是无论上述变量r实际存储的是什么类型,r的类型永远都是io.Reader,这就是为什么说Golang是静态类型编程语言,因为r声明时是io.Reader,在编译期就已经明确了类型。

Interface一个特别重要的示例是空接口:

interface{}

它代表一个空的方法集合,因为任意类型值都有0个多少多个方法,所以空的接口interface{}可以存储任意类型值。

有些人说Golang的interface是动态类型,其实是种误解。接口是静态类型,interface变量定义时就声明了一种静态类型,即便interface存储的值在运行时会改变类型,但是interface的类型是一定的。

一个interface类型变量会存储一对数据,具体类型的值和值的具体类型(value, concrete type)。例如:

var r io.Reader

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

if err != nil {

    return nil, err

}

r = tty

上述的interface变量I会存储一对数据(tty,*os.File)。需要注意的是*os.File类型不止单单实现了Read方法,还实现了其他方法,比如Write方法。即便interface类型变量i值提供了访问Read的方法,i还是携带了*os.File变量的所有类型信息。所以可以将i转换为io.Writer类型:

var w io.Writer

w = r.(io.Writer)

上述的表达式是一个类型断言,断言r也实现了io.Writer,所以可以赋值给w,否则会panic。完成赋值后,w会携带一对值(tty,*os.File),和r一样的一对值。接口的静态类型决定了上述的tty能够调用的方法,即便它实际上包含了更多的方法。

也可以将它赋值给空接口:

var empty interface{}

empty = w

空接口empty也携带同样的对值(tty,*os.File)。因为任意的类型都是空接口所以不用转换。

反射reflection

从本质上讲,反射是校验接口存储(value,concrete type)值对的一种机制。分别对应的reflect包的Value和Type类型。通过Value和Type类型可以访问到interface变量的储存内容,reflect.TypeOf和reflect.ValueOf将会返回interface变量的reflect.Type和reflect.Value类型值。

从TypeOf开始:

package main

import (

    "fmt"

    "reflect"

)

func main() {

    var x float64 = 3.4

    fmt.Println("type:", reflect.TypeOf(x))

}

结果将会输出:

type: float64

你可能会有疑问,反射是基于interface,那么这里的interface在哪儿呢?这就需要了解TypeOf的定义:

// TypeOf returns the reflection Type of the value in the interface{}.

func TypeOf(i interface{}) Type

也就是说TypeOf会用interface{}把参数储存起来,然后reflect.TypeOf再从interface{}中获取信息。

同理ValueOf的函数定义为:

// 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

示例:

var x float64 = 3.4

v := reflect.ValueOf(x)

fmt.Println("type:", v.Type())

fmt.Println("kind is float64:", v.Kind() == reflect.Float64)

fmt.Println("value:", v.Float())

结果输出:

type: float64

kind is float64: true

value: 3.4 

所以我们可以得出反射的第一条规则是:反射对象是从接口值获取的。

规则2:可以从反射对象中获取接口值。

利用reflect.Value的Interface方法可以获得传递过来的空接口interface{}:

// Interface returns v's value as an interface{}.

func (v Value) Interface() interface{}

示例:

y := v.Interface().(float64) // y will have type float64.

fmt.Println(y)

规则3:通过反射对象的set方法可以修改实际储存的变量,前提是存储的变量是可以被修改的。

反射定义变量是可以被修改的(settable)条件是传递变量的指针,因为如果是值传递的话,反射对象set方法改变的是一份拷贝,所以会显得怪异而且没有意义,所以干脆就将值传递的情况定义为不可修改的,如果尝试修改就会触发panic。

示例:

var x float64 = 3.4

v := reflect.ValueOf(x)

v.SetFloat(7.1) // Error: will panic

报错如下:

panic: reflect.Value.SetFloat using unaddressable value

可以通过反射对象Value的CanSet方法判断是否是可修改的:

var x float64 = 3.4

v := reflect.ValueOf(x)

fmt.Println("settability of v:", v.CanSet())

输出:

settability of v: false

可被修改的情况:

var x float64 = 3.4

p := reflect.ValueOf(&x) // Note: take the address of x.

fmt.Println("type of p:", p.Type())

fmt.Println("settability of p:", p.CanSet())

输出:

type of p: *float64

settability of p: false

反射对象p是不可被修改的,因为p不是我们想要修改的,*p才是。调用Value的Elem方法可以获取p指向的内容,并且内容储存在Value对象中:

v := p.Elem()

fmt.Println("settability of v:", v.CanSet())

输出:

settability of v: true

示例:

v.SetFloat(7.1)

fmt.Println(v.Interface())

fmt.Println(x)

输出:

7.1

7.1

结构体

只要有结构体的地址我们就可以用反射修改结构体的内容。下面是个简单的示例:

type T struct {

    A int

    B string

}

t := T{23, "skidoo"}

s := reflect.ValueOf(&t).Elem()

typeOfT := s.Type()

for i := 0; i < s.NumField(); i++ {

    f := s.Field(i)

    fmt.Printf("%d: %s %s = %v\n", i,

        typeOfT.Field(i).Name, f.Type(), f.Interface())

}

程序输出:

0: A int = 23

1: B string = skidoo

修改:

s.Field(0).SetInt(77)

s.Field(1).SetString("Sunset Strip")

fmt.Println("t is now", t)

程序输出:

t is now {77 Sunset Strip}

所以反射的三条规则总结如下:

规则1:反射对象是从接口值获取的。

规则2:可以从反射对象中获取接口值。

规则3:通过反射对象的set方法可以修改实际储存的settable变量

Json

由于Json的序列化(编码)和反序列化(解码)都会用到反射,所以这里放在一起讲解。

Json编码

可以用Marshal函数完成Json编码:

func Marshal(v interface{}) ([]byte, error)

给定一个Golang的结构体Message:

type Message struct {

    Name string

    Body string

    Time int64

}

Message的实例m为:

m := Message{"Alice", "Hello", 1294706395881547000}

Marshal编码Json:

b, err := json.Marshal(m)

如果工作正常,err为nil,b为[]byte类型的Json字符串:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

Json编码规则:

1.Json对象只支持string作为key;所以想要编码Golang map类型必须是map[stirng]T,其中T表示Golang支持的任意类型。

2.Channel,complex和函数类型不能被编码

3.循环引用嵌套的结构体不支持,他们会造成Marshal进入一个未知的循环体重

4.指针将会被编码指向的内容本身,如果指针是nil将会是null

Json解码

可以用Unmarshal解码Json数据:

func Unmarshal(data []byte, v interface{}) error

首先我们必须要先创建解码数据存储的变量:

var m Message

然后传递变量的指针(参考反射规则3):

err := json.Unmarshal(b, &m)

如果b包含可用的Json并且适合m,那么err将会是nil,b的数据会被存储在m中,就好像下面的赋值一样:

m = Message{

    Name: "Alice",

    Body: "Hello",

    Time: 1294706395881547000,

}

Unmarshal是怎么识别要存储的解码字段的呢?例如Json的一个Key为”Foo”,Unmarshal会找根据下面的规则顺序匹配:

1.找名为“Foo”的字段tag

2.找名为“Foo”,”FOO”或者“FoO”的字段名称

再看下面的Json数据解码会匹配到Golang的什么数据类型呢:

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)

var m Message

err := json.Unmarshal(b, &m)

Unmarshal只会解码它认识的字段。在这个例子中,只有Name字段出现在m中,所以Food字段会被忽略。当你想在一个大的Json数据中提取你要想的部分字段时,该特性是非常有用的。这意味着你不需要关心Json的所有字段,只需要关心你要用到的字段即可。

json包会用map[string]interface{}存储Json对象,用[]interface{}存储数组。当Unmarshal Json对象作为interface{}值时,默认Golang的concrete type为:

Json booleans类型默认为bool

Json 数字默认为float64

Json strings默认为string

Json null默认为nil

示例:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

var f interface{}

err := json.Unmarshal(b, &f)

相对于下面的赋值操作:

f = map[string]interface{}{

    "Name": "Wednesday",

    "Age":  6,

    "Parents": []interface{}{

        "Gomez",

        "Morticia",

    },

}

如果想要访问f的底层map[string]interface{}数据结构需要断言:

m := f.(map[string]interface{})

然后遍历map接着访问其他成员:

for k, v := range m {

    switch vv := v.(type) {

    case string:

        fmt.Println(k, "is string", vv)

    case float64:

        fmt.Println(k, "is float64", vv)

    case []interface{}:

        fmt.Println(k, "is an array:")

        for i, u := range vv {

            fmt.Println(i, u)

        }

    default:

        fmt.Println(k, "is of a type I don't know how to handle")

    }

}

上述示例中,可以定义一个结构体来存储:

type FamilyMember struct {

    Name    string

    Age     int

    Parents []string

}

var m FamilyMember

err := json.Unmarshal(b, &m)

Unmarshal数据进入FamilyMembear值时,会自动给nil 切片分配内存,同理如果有指针,map也会自动分配内存。

总结

文章介绍了interface、reflection、json,其中reflection是基于interface实现的,而json的编码和解码用到了reflection。

参考

https://blog.golang.org/json-and-go

https://blog.golang.org/laws-of-reflection

https://juejin.im/post/5a75a4fb5188257a82110544?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ZooKeeper和ZAB协议

    ZooKeeper是一个提供高可用,一致性,高性能的保证读写顺序的存储系统。ZAB协议为ZooKeeper专门设计的一种支持数据一致性的原子广播协议。

    用户2937493
  • 开源监控系统Prometheus介绍

    Prometheus是CNCF的一个开源项目,Google BorgMon监控系统的开源版本,是一个系统和服务的监控系统。周期性采集metrics指标,匹配规则...

    用户2937493
  • Pinpoint 一款强大的APM工具

    程序的监控一直是程序员最头痛的事情之一,现网程序有问题怎么办?看进程看端口 top/free/df 三件套?网络抓包?看日志?所以为了满足这些初级需求很多公司都...

    用户2937493
  • Go 语言面向对象教程 —— 接口篇:空接口及使用场景

    熟悉 Java 的同学应该都知道,在这个号称血统最纯正的面向对象编程语言中,「万事万物皆对象」,并且所有类都继承自祖宗类「Object」,所以 Object 类...

    学院君
  • 【Go 语言社区】Golang 可变参数的使用

    func main() { Func1(1,2,3,4) } func Func1(args ...int) { for _, ...

    李海彬
  • 【一起学源码-微服务】Feign 源码一:源码初探,通过Demo Debug Feign源码

    上一讲深入的讲解了Ribbon的初始化过程及Ribbon与Eureka的整合代码,与Eureka整合的类就是DiscoveryEnableNIWSServerL...

    一枝花算不算浪漫
  • 小程序开发踩坑指南

    小程序组件分为原生组件和非原生组件,原生组件属于客户端的组件,在WebView的渲染流程之外的,且层级在所有非原生组件之上(无论你如何改z-index都没用的)...

    Clayman Twinkle
  • Java8学习之Stream(流)

    本文讲述.stream()的内容,需要一些Lambda表达式的基础,之前也推送过关于Lambda表达式和Stream的相关内容,就看哪盘菜味道更好!

    用户5927304
  • 工作后, 你一定不能错过技术之JDK1.8的新特性

    而在企业开发中, 主要用到的便是Lambda表达式和Stream流 , 而在下面,我们便主要的去学习这两方面的知识

    时间静止不是简史
  • 位运算判断奇偶数

    以前写判断奇偶数的函数都是用retrun(0 == n%2 ? TRUE : FALSE)这句话写。后来我想,一个整数的二进制码最后一位,奇数是1,偶数是0。我...

    phith0n

扫码关注云+社区

领取腾讯云代金券