我终于识破了这个 Go 编译器把戏

2024-01-09 18:38

本文主要是介绍我终于识破了这个 Go 编译器把戏,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在 Go 语言的日常编码工作中,有一个非常普遍但诡异的编译错误,曾让我十分困惑。这个问题我相信不少 Gopher 都遇到过,不妨来看一下。

背景回顾

我们定义一个带有 WriteGoCode() 方法的 Gopher 接口,同时定义了 person 结构体,它存在 WriteGoCode() 方法。

type Gopher interface {WriteGoCode()
}type person struct {name string
}func (p person) WriteGoCode() {fmt.Printf("I am %s, i am writing go code!\n", p.name)
}

在 Go 语言中,只要某对象拥有接口的所有方法,那该对象即实现了该接口。pperson 结构体的实例化对象, Coding() 函数的入参是 Gopher 接口, person 对象实现了 Gopher 接口,因此 p 入参成功被运行。

func Coding(g Gopher) {g.WriteGoCode()
}func main() {p := person{name: "小菜刀"}Coding(p)
}// output:
I am 小菜刀, i am writing go code!

此时,我们将 Coding() 函数的入参改为 []Gopher 类型,入参为 []person

func Coding(gs []Gopher) {for _, g := range gs {g.WriteGoCode()}
}func main() {p := []person{{name: "小菜刀1号"},{name: "小菜刀2号"},}Coding(p)
}

但是,这个时候,编译却不能通过!

./main.go:29:8: cannot use p (type []person) as type []Gopher in argument to Coding

明明 person 类型实现了 Gopher 接口,且当函数入参为 Gopher 类型时,能够顺利被执行,但参数变为 []Gopher 时,却过不了编译,这是为什么?

语法通用规则

这个问题在 stackoverflow 上被热议,详情见文末参考链接1。

在 Go 中,有一个通用规则,即语法不应隐藏复杂/昂贵的操作。转换一个 stringinterface{} 它的时间复杂度是 O(1),转换 []stringinterface{} 同样也是一个 O(1) 操作,因为它还是一个单一值的转换。

如果要将 []string 转换为 []interface{},它是 O(N) 操作。因为切片的每个元素都必须转换为 interface{},这违背了 Go 的语法原则。

这个回答,你们同意吗?

当然,此规则存在一个例外:转换字符串。在将 string 转换为 []byte[]rune 时,即使需要 O(n) 操作,但 Go 会允许执行。

InterfaceSlice 问题

Ian Lance Taylor(Go 核心开发者) 在 Go 官方仓库中也回答了这个问题,详情见文末参考链接2。他给出了这样做的两个主要原因。

  • 原因一:类型为 []interface{} 的变量不是 interface!它仅仅是一个元素类型恰好为 interface{} 的切片。

  • 原因二:[]interface{} 变量有特定大小的内存布局,在编译期可知。这与 []MyType 是不同的。

每个 interface{} (运行时通过 runtime.eface 表示)占两个字长(一个字代表所包含内容的类型 _type,另外一个字表示所包含的数据 data 或者指向它的指针 )

因此,类型为 []interface{} 的长度为 N 的变量,它是由 N*2 个字长的数据块支持。而这与类型为 []MyType 的长度为 N 的变量的数据块大小是不同的,因为后者的数据块是 N*sizeof(MyType) 字长。

数据块的不同,造成的结果是编译器无法快速地将 []MyType 类型的内容分配给 []interface{} 类型的内容。

同理,[]Gopher 变量也是特定大小的内存布局(运行时通过 runtime.iface 表示)。这同样不能快速地将 []MyType 类型的内容分配给 []Gopher 类型。

因此,Ian Lance Taylor 回答闭环了 Go 的语法通用规则:Go 语法不应隐藏复杂/昂贵的操作,编译器会拒绝它们。

代码解决方案

再次将文章开头的例子附上,如果我们需要 [] person 类型的 p 能够成功入参 Coding() 函数,应该如何做呢。

func Coding(gs []Gopher) {for _, g := range gs {g.WriteGoCode()}
}func main() {p := []person{{name: "小菜刀1号"},{name: "小菜刀2号"},}Coding(p)
}

代码方案如下,核心是需要一个 []Gopher 类型的转换变量。

func main() {p := []person{{name: "小菜刀1号"},{name: "小菜刀2号"},}var interfaceSlice []Gopher = make([]Gopher, len(p))for i, g := range p {interfaceSlice[i] = g}Coding(interfaceSlice)
}// output:
I am 小菜刀1号, i am writing go code!
I am 小菜刀2号, i am writing go code!

总结

由于 []MyType[]interface{} 的转换,是昂贵的操作,Go 编译器不会允许这种情况通过编译,故而将这种开销的责任传递给开发者。

Go 是一门编译速度很快的语言,得益于它语法设计中贯彻着 “simpler is better” 的理念,这可不是说说而已。

参考链接

【1. Type converting slices of interfaces】

https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces/12754757#12754757

【2. InterfaceSlice】

https://github.com/golang/go/wiki/InterfaceSlice

往期推荐

机器铃砍菜刀

欢迎添加小菜刀微信

加入Golang分享群学习交流!

感谢你的点赞在看哦~

这篇关于我终于识破了这个 Go 编译器把戏的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588055

相关文章

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

Linux编译器--gcc/g++使用方式

《Linux编译器--gcc/g++使用方式》文章主要介绍了C/C++程序的编译过程,包括预编译、编译、汇编和链接四个阶段,并详细解释了每个阶段的作用和具体操作,同时,还介绍了调试和发布版本的概念... 目录一、预编译指令1.1预处理功能1.2指令1.3问题扩展二、编译(生成汇编)三、汇编(生成二进制机器语

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Go Gorm 示例详解

《GoGorm示例详解》Gorm是一款高性能的GolangORM库,便于开发人员提高效率,本文介绍了Gorm的基本概念、数据库连接、基本操作(创建表、新增记录、查询记录、修改记录、删除记录)等,本... 目录1. 概念2. 数据库连接2.1 安装依赖2.2 连接数据库3. 数据库基本操作3.1 创建表(表关