go 语言 interface{} 的易错点
一,interface 介绍
如果说 goroutine 和 channel 是 go 语言并发的两大基石,那 interface 就是 go 语言类型抽象的关键。在实际项目中,几乎所有的数据结构最底层都是接口类型。说起 C++ 语言,我们立即能想到是三个名词:封装、继承、多态。go 语言虽然没有严格意义上的对象,但通过 interface,可以说是实现了多态性。(由以组合结构体实现了封装、继承的特性)
go 语言中支持将 method、struct、struct 中成员定义为 interface 类型,使用 struct 举一个简单的栗子
package main
type animal interface {
Move()
}
type bird struct{}
func (self *bird) Move() {
println("bird move")
}
type beast struct{}
func (self *beast) Move() {
println("beast move")
}
func animalMove(v animal) {
v.Move()
}
func main() {
var a *bird
var b *beast
animalMove(a) // bird move
animalMove(b) // beast move
}
使用 go 语言的 interface 特性,就能实现多态性,进行泛型编程。
二,interface 原理
如果没有充分了解 interface 的本质,就直接使用,那最终肯定会踩到很深的坑,要用就先要了解,先来看看 interface 源码
type eface struct {
_type *_type
data unsafe.Pointer
}
type _type struct {
size uintptr // type size
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldalign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
可以看到 interface 变量之所以可以接收任何类型变量,是因为其本质是一个对象,并记录其类型和数据块的指针。(其实 interface 的源码还包含函数结构和内存分布,由于不是本文重点,有兴趣的同学可以自行了解)
三,interface 判空的坑
对于一个空对象,我们往往通过 if v == nil 的条件语句判断其是否为空,但在代码中充斥着 interface 类型的情况下,很多时候判空都并不是我们想要的结果(其实了解或聪明的同学从上述 interface 的本质是对象已经知道我想要说的是什么)
package main
type animal interface {
Move()
}
type bird struct{}
func (self *bird) Move() {
println("bird move")
}
type beast struct{}
func (self *beast) Move() {
println("beast move")
}
func animalMove(v animal) {
if v == nil {
println("nil animal")
}
v.Move()
}
func main() {
var a *bird // nil
var b *beast // nil
animalMove(a) // bird move
animalMove(b) // beast move
}
还是刚才的栗子,其实在 go 语言中 var a *bird 这种写法,a 只是声明了其类型,但并没有申请一块空间,所以这时候 a 本质还是指向空指针,但我们在 aminalMove 函数进行判空是失败的,并且下面的 v.Move() 的调用也是成功的,本质的原因就是因为 interface 是一个对象,在进行函数调用的时候,就会将 bird 类型的空指针进行隐式转换,转换成实例的 interface animal 对象,所以这时候 v 其实并不是空,而是其 data 变量指向了空。这时候看着执行都正常,那什么情况下坑才会绊倒我们呢?只需要加一段代码
package main
type animal interface {
Move()
}
type bird struct {
name string
}
func (self *bird) Move() {
println("bird move %s", self.name) // panic
}
type beast struct {
name string
}
func (self *beast) Move() {
println("beast move %s", self.name) // panic
}
func animalMove(v animal) {
if v == nil {
println("nil animal")
}
v.Move()
}
func main() {
var a *bird // nil
var b *beast // nil
animalMove(a) // panic
animalMove(b) // panic
}
在代码中,我们给派生类添加 name 变量,并在函数的实现中进行调用,就会发生 panic,这时候的 self 其实是 nil 指针。所以这里坑就出来了。有些人觉得这类错误谨慎一些还是可以避免的,那是因为我们是正向思维去代入接口,但如果反向编程就容易造成很难发现的 bug
package main
type animal interface {
Move()
}
type bird struct {
name string
}
func (self *bird) Move() {
println("bird move %s", self.name)
}
type beast struct {
name string
}
func (self *beast) Move() {
println("beast move %s", self.name)
}
func animalMove(v animal) {
if v == nil {
println("nil animal")
}
v.Move()
}
func getBirdAnimal(name string) *bird {
if name != "" {
return &bird{name: name}
}
return nil
}
func main() {
var a animal
var b animal
a = getBirdAnimal("big bird")
b = getBirdAnimal("") // return interface{data:nil}
animalMove(a) // bird move big bird
animalMove(b) // panic
}
这里我们看到通过函数返回实例类型指针,当返回 nil 时,因为接收的变量为接口类型,所以进行了隐性转换再次导致了 panic(这类反向转换很难发现)。
那我们如何处理上述这类问题呢。我这边整理了三个点
1,充分了解 interface 原理,使用过程中需要谨慎小心
2,谨慎使用泛型编程,接收变量使用接口类型,也需要保证接口返回为接口类型,而不应该是实例类型
3,判空是使用反射 typeOf 和 valueOf 转换成实例对象后再进行判空
go 语言 interface{} 的易错点的更多相关文章
- C语言指针的易错点
1.内存泄漏:申请的堆内存没有释放. 2.内存污染:前面非法操作使用内存(没有报错),后面写着写着就出错.如下代码: 当结构体中只有划线部分代码时,在编译器中编写不会报错,但此时已经造成非法操作内存, ...
- R语言 几个易错的地方
1.列表与向量 定义一个向量,然后向内添加元素,得到一个长向量列表: > a = c() #定义一向量 > for (i in 1:5) + a = c(a,i) > a [1] 1 ...
- c语言的一些易错知识积累
1. #ifdef 和#if defined 的区别: 后者可以组成复杂的预编译条件,而如果判断的是单个宏定义的时候,两种用法的效果都是一样的. 2.#if 0 { code }#endif ...
- *C语言有关指针的变量声明中的几个易错点
转至:http://my.oschina.net/ypimgt/blog/108265 Technorati 标签: 指针, typedef, const, define 我们都知道,至少听说过 ...
- C语言易错点
C语言易错点 1.每个C语言程序中main函数是有且只有一个的. 2.算法可以没有输入,但必须要有输出. 3.在函数中不可以再定义函数. 4.break可用于循环结构和switch语句. 5.brea ...
- 细节!重点!易错点!--面试java基础篇(二)
今天来给大家分享一下java的重点易错点第二部分,也是各位同学面试需要准备的,欢迎大家交流指正. 1.字符串创建与存储机制:当创建一个字符串时,首先会在常量池中查找是否已经有相同的字符串被定义,其判断 ...
- 细节!重点!易错点!--面试java基础篇(一)
今天来给大家分享一下java的重点易错点部分,也是各位同学面试需要准备的,欢迎大家交流指正. 1.java中的main方法是静态方法,即方法中的代码是存储在静态存储区的. 2.任何静态代码块都会在ma ...
- 关于Verilog HDL的一些技巧、易错、易忘点(不定期更新)
本文记录一些关于Verilog HDL的一些技巧.易错.易忘点等(主要是语法上),一方面是方便自己忘记语法时进行查阅翻看,另一方面是分享给大家,如果有错的话,希望大家能够评论指出. 关键词: ·技巧篇 ...
- JavaScript易错知识点整理
前言 本文是我学习JavaScript过程中收集与整理的一些易错知识点,将分别从变量作用域,类型比较,this指向,函数参数,闭包问题及对象拷贝与赋值这6个方面进行由浅入深的介绍和讲解,其中也涉及了一 ...
随机推荐
- 最近在研究IO
import java.io.File; import java.io.IOException; public class Demo11_1 { public static void main(Str ...
- FFmpeg常用命令学习笔记(三)分解/复用命令
分解/复用命令 比如文件格式的转换.将封装格式文件中的音频与视频文件分别抽取出来等. 多媒体格式的转换(将MP4文件转成flv格式) ffmpeg -i yan.mp4 -vcodec copy -a ...
- C# 继承(3)持续更新
类继承 和 接口继承 类继承 一个类型派生于一个基类行,它拥有该基类型的所有成员字段和函数. 接口继承 一个类型继承函数的签名,不需要实现代码. 多重继承 一个类派生自多个类.多 ...
- HTML中dl元素的高度问题
dl元素通常用来创建一个描述列表,但是在我使用的过程中发现了一个小问题. 定义及用法 在MDN中 <dl> 元素的定义是:一个包含术语定义以及描述的列表,通常用于展示词汇表或者元数据 (键 ...
- 微雪的stm32学习资料
http://www.waveshare.net/wiki/Main_Page里面有很多资料 STM32开发软件 目录 编译软件 Keil MDKSTM32CubeMX 下载软件 STM32 ISP ...
- C# HttpClient使用 网络(一)
一.异步调用web服务 GetAsync() static void Main(string[] args) { Console.WriteLine("In main before cal ...
- C# Transaction 事务处理 -依赖事务
在DependentTransaction()方法中,实例化CommittableTransaction类,创建一个根事务,显示事务的信息.接着, tx.DependentClone()方法创建一个依 ...
- Codeforces Round #591 (Div. 2, based on Technocup 2020 Elimination Round 1) A. CME
链接: https://codeforces.com/contest/1241/problem/A 题意: Let's denote correct match equation (we will d ...
- 01-vue和api整合流程、CORS
1.后端代码 1.项目结构 2.项目代码 主url from django.contrib import admin from django.urls import path, include url ...
- MySQL多表查询总结
MySQL术语: Redundacncy(冗余):存储两次或多次数据,以便实现快速查询. Primary Key(主键):主键是唯一的.表中每条记录的唯一标识. Foreign Key(外键):用于连 ...