前言

反射是程序校验自己数据结构和类型的一种机制。文章尝试解释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, )

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{, "skidoo"}

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

typeOfT := s.Type()

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

    f := s.Field(i)

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

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

}

程序输出:

: A int = 

: B string = skidoo

修改:

s.Field().SetInt()

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

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

程序输出:

t is now { 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", }

Marshal编码Json:

b, err := json.Marshal(m)

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

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

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: ,

}

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":,"Parents":["Gomez","Morticia"]}`)

var f interface{}

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

相对于下面的赋值操作:

f = map[string]interface{}{

    "Name": "Wednesday",

    "Age":  ,

    "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

Golang高效实践之interface、reflection、json实践的更多相关文章

  1. Golang高效实践之泛谈篇

    前言 我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如: <Golang高效实践之interface.reflection.json实践>&l ...

  2. Golang 高效实践之并发实践context篇

    前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控 ...

  3. IntelliJ IDEA:Getting Started with Spring MVC, Hibernate and JSON实践

    原文:IntelliJ IDEA:Getting Started with Spring MVC, Hibernate and JSON实践 最近把编辑器换成IntelliJ IDEA,主要是Ecli ...

  4. Golang在视频直播平台的高性能实践

    http://toutiao.com/i6256894054273909249/ 熊猫 TV 是一家视频直播平台,先介绍下我们系统运行的环境,下面这 6 大服务只是我们几十个服务中的一部分,由于并发量 ...

  5. Golang面向API编程-interface(接口)

    Golang面向API编程-interface(接口) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Golang并不是一种典型的面向对象编程(Object Oriented Pr ...

  6. Linux课程实践一:Linux基础实践(基础操作)

    一.软件源维护 1. 基本操作 (1)查看源列表 sudo vim /etc/apt/sources.list deb:二进制软件安装包 deb-src:源码包 (2)备份软件源列表 sudo cp ...

  7. 机器学习实践:《Python机器学习实践指南》中文PDF+英文PDF+代码

    机器学习是近年来渐趋热门的一个领域,同时Python 语言经过一段时间的发展也已逐渐成为主流的编程语言之一.<Python机器学习实践指南>结合了机器学习和Python 语言两个热门的领域 ...

  8. Golang 高效实践之并发实践

    前言 在我前面一篇文章Golang受欢迎的原因中已经提到,Golang是在语言层面(runtime)就支持了并发模型.那么作为编程人员,我们在实践Golang的并发编程时,又有什么需要注意的点呢?下面 ...

  9. Golang 高效实践之defer、panic、recover实践

    前言 我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑.但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃.Golang有没有一种异常 ...

随机推荐

  1. Protoc Buffer 优化传输大小的一个细节

    Protoc Buffer 是我们比较常用的序列化框架,Protocol Buffer 序列化后的占空间小,传输高效,可以在不同编程语言以及平台之间传输.今天这篇文章主要介绍 Protocol Buf ...

  2. spring 5.x 系列第8篇 —— 整合Redis客户端 Jedis和Redisson (代码配置方式)

    文章目录 一.说明 1.1 Redis 客户端说明 1.2 Redis可视化软件 1.3 项目结构说明 1.3 依赖说明 二.spring 整合 jedis 2.1 新建基本配置文件和其映射类 2.2 ...

  3. mysql-8.0-winx64安装以及修改密码

    一.下载安装包(https://dev.mysql.com/downloads/mysql/) 二.添加my.ini配置文件 打开刚刚解压的文件夹 C:\mysql-8.0.16-winx64,在该文 ...

  4. idea提交代码到自己git账号的master branch

    1.注册GitHub账号 2.创建本地密钥与远程仓库连接(使用idea的话,这第二步可能不是必须的,但是密钥我之前配置过.所以写下来) ①安装git 客户端sudo apt-get install g ...

  5. 正则RegExp对象的用法

    RegExp实例方法: 1.Test() RegExpObject.test(string) 判断string中是否有与表达式匹配的字符串,有则返回true,否则返回false 例如 var patt ...

  6. PATB 1028. 人口普查(20)

    1028. 人口普查(20) 注意特判合理人数为0,否则格式错误.很暴力的sort排序找出最大最小. 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Stan ...

  7. GitLab通过API创建项目

    示例: #!/usr/bin/python # -*- coding:utf-8 -*- import os import json import requests import subprocess ...

  8. HashMap原理(一) 概念和底层架构

    HashMap在Java开发中使用的非常频繁,可以说仅次于String,可以和ArrayList并驾齐驱,准备用几个章节来梳理一下HashMap.我们还是从定义一个HashMap开始. HashMap ...

  9. 03_javaSE面试题:类初始化和实例初始化

    题目 下面代码运行的结果是什么? Father 类 /** * @author kevin * @date 2019/7/8 15:48 */ public class Father { privat ...

  10. PyCharm2018 汉化&激活

    一.汉化 将下载好的resources_cn_PyCharm_2018.1_r2.jar 放入pycharm 的lib 目录中,启动app即可 下载链接: https://pan.baidu.com/ ...