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, )
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
Golang高效实践之interface、reflection、json实践的更多相关文章
- Golang高效实践之泛谈篇
前言 我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如: <Golang高效实践之interface.reflection.json实践>&l ...
- Golang 高效实践之并发实践context篇
前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控 ...
- IntelliJ IDEA:Getting Started with Spring MVC, Hibernate and JSON实践
原文:IntelliJ IDEA:Getting Started with Spring MVC, Hibernate and JSON实践 最近把编辑器换成IntelliJ IDEA,主要是Ecli ...
- Golang在视频直播平台的高性能实践
http://toutiao.com/i6256894054273909249/ 熊猫 TV 是一家视频直播平台,先介绍下我们系统运行的环境,下面这 6 大服务只是我们几十个服务中的一部分,由于并发量 ...
- Golang面向API编程-interface(接口)
Golang面向API编程-interface(接口) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Golang并不是一种典型的面向对象编程(Object Oriented Pr ...
- Linux课程实践一:Linux基础实践(基础操作)
一.软件源维护 1. 基本操作 (1)查看源列表 sudo vim /etc/apt/sources.list deb:二进制软件安装包 deb-src:源码包 (2)备份软件源列表 sudo cp ...
- 机器学习实践:《Python机器学习实践指南》中文PDF+英文PDF+代码
机器学习是近年来渐趋热门的一个领域,同时Python 语言经过一段时间的发展也已逐渐成为主流的编程语言之一.<Python机器学习实践指南>结合了机器学习和Python 语言两个热门的领域 ...
- Golang 高效实践之并发实践
前言 在我前面一篇文章Golang受欢迎的原因中已经提到,Golang是在语言层面(runtime)就支持了并发模型.那么作为编程人员,我们在实践Golang的并发编程时,又有什么需要注意的点呢?下面 ...
- Golang 高效实践之defer、panic、recover实践
前言 我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑.但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃.Golang有没有一种异常 ...
随机推荐
- Codility---MaxProductOfThree
Task description A non-empty zero-indexed array A consisting of N integers is given. Theproduct of t ...
- 【vue系列】Virtual DOM 真的比操作原生 DOM 快吗?
一.前言 网上都说操作真实dom怎么怎么慢,这儿有个例子:http://chrisharrington.github.io/demos/performance/,例子循环2000个随机数组,点击按钮重 ...
- vue+element——父级元素fixed,遮罩会在上方
前言 这种场景还是蛮场景的 一个共用的head组件,组件里面通常是当前系统登录账号名 退出登录 修改密码这样的弹框 但是现在我又想head不跟着main内容上下滑动.所以用了fixed 定位. 问题来 ...
- fork和僵尸进程
1. 关于fork fork()函数: 用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息:在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行 ...
- Metasploit渗透测试
原创博客,转载请注出处! 学习笔记 参考书籍<Metasploit渗透测试指南(修订版)> 经过多日学习,初步掌握metasploit基本参数和使用方法,现进行渗透测试实践 靶机IP:16 ...
- 丢给你一个txt并同时获取你shell
丢给你一个txt并同时获取你shell 0x00:回顾 <文本编辑器Vim/Neovim被曝任意代码执行漏洞> 听闻很多人知道这个漏洞,但是有一部分人能复现成功,一部分人复现不出来.这里我 ...
- php回调函数设计
<?php namespace Server; /** * 回调函数设计 * Class Server * @package Server */ class Server { public fu ...
- jQuery入门——注册事件
下面举例介绍注册事件的几种方法: 以光棒效果为例 1.bind注册: <!DOCTYPE html> <html> <head> <meta charset= ...
- [Qt]自定义表头实现过滤功能
1. 写在前面 过滤功能源自项目上交互优化用户体验,在表头添加过滤符号实现过滤,替换以往在表格上方占用一行过滤项进行过滤. 2. 过滤提示 过滤提示就是三态图标(normal,hover,press) ...
- mount -- 挂载理解
1.挂载? 在windows操作系统中, 挂载通常是指给磁盘分区(包括被虚拟出来的磁盘分区)分配一个盘符. 第三方软件,如磁盘分区管理软件.虚拟磁盘软件等,通常也附带挂载功能. 在linux操作系统中 ...