在刚开始学习Go语言的过程中,难免会遇到一些问题,尤其是从其他语言转向Go开发的人员,面对语法及其内部实现的差异,在使用Go开发时也避免不了会踩“坑”。本文主要针对Go设计中的屏蔽现象进行详细的说明,我主要从变量屏蔽和方法屏蔽的角度去分析Go中的屏蔽现象。
程序实体的作用域
在开始分析变量的屏蔽之前,我们先来理解一下Go语言中的作用域,Go语言的作用域决定了一个程序实体可以被访问的范围。在Go语言的组织架构中,程序实体被层层嵌套的代码块包裹,正是这些代码块决定了程序实体的访问权限,Go 语言中程序实体的访问权限主要体现三种形式:包级别私有,模块级别私有(internal)以及公开的访问权限。包级别私有和模块级别私有对应的是代码包代码块,而公开的访问权限则是对应全域代码块。然而更细粒度的权限控制代码块还体现在函数,循环体内等。在Go语言层面其实就是依据代码块对程序实体作用域进行的定义。
变量的屏蔽现象
根据对作用域的理解,程序实体的访问权限由代码块控制,变量也属于程序实体, 嵌套的代码块导致变量出现被屏蔽的现象。
package main
import "fmt"
var name = struct{}{}
func main() {
name := "vitamin "
{
name := 0
fmt.Println(name)
}
fmt.Println(name)
}
点击运行
main包中的name变量被main函数中的同名变量“屏蔽”,而main函数中的name变量又被子代码块内部的声明的同名变量所“屏蔽”,而且这里的同名变量类型可以不同。为什么会出现这种“屏蔽”现象?其实这也暗示了Go语言变量的查找过程,当然这个过程并不限制于变量,而是适用于所有程序实体。
方法的屏蔽现象
在Go语言中方法是定义在某个自定义数据类型(不能是接口类型)上的特殊函数,那在方法的定义上会不会出现和变量一样的“屏蔽”现象呢,让我们以结构体为例看几个例子
在结构体内定义同名的字段和方法
type Superhero struct{
FightWay string
}
func(c Superhero) FightWay()string{
return c.FightWay
}
func main() {
s:=Superhero{}
fmt.Println(s.FightWay)
}
点击运行
运行代码后我们会看到这样的编译错误: "type Superhero has both field and method named FightWay",这也直接的说明了在结构体内部是不允许出现这种定义方式的,所以也根本不存在“屏蔽”的现象。
在结构体内定义同名但不同签名的方法
在某些编程语言中,这种定义方式可能叫做“重载”,是实现面向对象中多态的一种表现形式,那么在Go语言中又会是怎么样呢?
type Superhero struct{
FightWay string
}
func(c Superhero) FightWay()string{
return c.FightWay
}
func(c *Superhero) FightWay(way string){
c.FightWay = way
}
func main() {
s:=Superhero{}
fmt.Println(s.FightWay())
}
点击运行
运行代码后编译出现错误: "type Superhero has both field and method named FightWay",看来这种定义方式在Go语言中也是不允许的,所以也不存在“屏蔽”的现象。
在含内嵌类型的结构体和被内嵌类型结构体之中定义同名的属性
在Go语言中并不存在继承的概念,而是以一种更加灵活地方式:组合类型(内嵌类型),来实现对多个结构体之间的组合,并将内嵌类型的属性和方法嫁接到了组合类型,避免了继承这种关系导致结构体之间的强依赖和繁琐的多重继承问题。
type Superhero struct {
FightWay string
}
type Ironman struct {
FightWay string
Superhero
}
func main() {
i:=Ironman{ FightWay: "Steel Armor", Superhero:Superhero{ "The Avengers" }}
fmt.Println(i.FightWay)
}
点击运行
由输出结果可以看到组合的结构体(Ironman)内的 FightWay 属性“屏蔽”了被内嵌类型(Superhero)的同名属性
在含内嵌类型的结构体和被内嵌类型结构体之中定义同名的属性和方法
type Superhero struct {
FightWay string
}
type Ironman struct {
Superhero
}
func(i Ironman )FightWay()string{
return i.Superhero.FightWay
}
func main() {
i:=Ironman{ Superhero:Superhero{ "The Avengers" }}
fmt.Println(i.FightWay)
}
点击运行
运行程序后我们发现编译报错: “Println arg i.FightWay is a func value, not called”,这也间接的说明了组合类型(Ironman)中定义的方法 FightWay()string “屏蔽”了内嵌类型(Superhero)中的同名属性,,反过来我们尝试一下
type Superhero struct {
}
func(s Superhero)FightWay()string{
return "The Avengers"
}
type Ironman struct {
FightWay string
Superhero
}
func main() {
i:=Ironman{ FightWay:"Steel Armor" }
fmt.Println(i.FightWay())
}
点击运行
运行代码后我们同样会看到编译错误: "cannot call non-function i.FightWay (type string)",这也从反面论证了在组合类型和内嵌类型中声明同名的属性和方法之间的互相“屏蔽“现象。
在含内嵌类型的结构体和被内嵌类型结构体之中定义同名但不同签名的方法
type Superhero struct { }
func(s Superhero)FightWay()string{
return "The Avengers"
}
type Ironman struct {
Name string
Superhero
}
func(i *Ironman) FightWay(name string){
i.Name=name
}
func main() {
i:=Ironman{ Name:"Iron Man" }
i.FightWay()
fmt.Println(i.Name)
}
点击运行
运行代码后我们发现编译器报错: "not enough arguments in call to i.FightWay",在组合类型和被内嵌类型之间定义的同名不同签名的方法同样存在“屏蔽”现象。
上面我们演示的都是看上去不是在同一层面的结构,那么对于多个内嵌类型处于同一层面,这时会不会发生变量或者是方法的屏蔽现象呢
在同一层面的内嵌类型的结构体内定义同名的属性
type Superhero struct {
Name string
}
type Skill struct{
Name string
}
type Ironman struct {
Superhero
Skill
}
func main() {
i:=Ironman{ Superhero:Superhero{"Iron Man"},Skill:Skill{ Name:"Steel Armor" } }
fmt.Println(i.Name)
}
点击运行
当我们运行代码后,会发现编译器输出这样一句话:“ambiguous selector i.Name”,这是由于组合类型就是把内嵌类型内的变量嫁接到组合类型,此时在组合类型内存在两个一样的属性,当属性被调用的时候,编译器也不知道该调用哪一个,跟内嵌类型在组合类型内出现的顺序没有关系。当然编译不通过也就不会涉及“屏蔽”现象了。
在同一层面的内嵌类型的结构体内定义同名的变量和方法
type Superhero struct {
Name string
}
type Skill struct{}
func(s Skill) Name()string{
return "Hero`s Skill"
}
type Ironman struct {
Superhero
Skill
}
func main() {
i:=Ironman{ Superhero:Superhero{"Iron Man"} }
fmt.Println(i.Name)
}
点击运行
当我们运行代码之后,也是会出现编译错误:“ambiguous selector i.Name” 。接下来我们在看看方法会不会也出现类似的编译错误
在同一层面的内嵌类型的结构体内定义同名但不同签名的方法
type Superhero struct { }
func(s Superhero) Name()string{
return "Super Hero "
}
type Skill struct{}
func(s Skill) Name()(string,error){
return "Hero`s Skill",nil
}
type Ironman struct {
Superhero
Skill
}
func main() {
i:=Ironman{ }
fmt.Println(i.Name())
}
点击运行
运行代码后,诶!还是编译错误:“ambiguous selector i.Name”。
其实,无论组合类型中内嵌了多少个其他类型又或者是这些内嵌类型又内嵌了其他内嵌类型,出于最深层次的属性或者是方法被上层同名属性或方法“屏蔽”的概率就越高;当处于同一层面的多个内嵌类型之间含有同性的属性或者是方法时,从错误信息也可得知,编译器不知如何选择被调用的属性或者方法,从而引发编译错误,当然这也很容易理解。
好了,上面我们针对嵌入的类型都是非指针类型,如果在结构内嵌入的是指针类型又会是怎么样的呢?
在结构体内嵌入某个类型的指针类型
当我们在组合类型内嵌入某个自定义类型的指针类型时,仍然像上述那样出现各种“屏蔽”和编译不通过的现象。但是他们之间的区分在于值类型方法和指针类型的方法对应实现接口的是不相同的(不含实现0个的情况)。
如何访问被屏蔽的信息
对于访问被“屏蔽”的属性或者是方法,我们可以通过使用组合类型实例+“.”+内嵌类型的类型名称+“.”+被“屏蔽”的属性或方法的链式编程的形式访问到
type Superhero struct {
FightWay string
}
type Ironman struct {
FightWay string
Superhero
}
func main() {
i:=Ironman{ FightWay: "Steel Armor", Superhero:Superhero{ "The Avengers" }}
fmt.Println(i.Superhero.FightWay)
}
点击运行
运行代码后,我们会看到结果:“The Avengers”,而这个值正是被组合类型同名属性“屏蔽”掉属性的值。
屏蔽可以带来哪些好处
虽然Go语言设计形式允许存在“屏蔽”现象,在“屏蔽”的同时也给我们带来了一些好处,下面我们来谈谈:
组合类型的方法可以对内嵌类型的同名方法进行包装,扩展,这也是在面向对象开发过程中常用的一种手法。
type Task struct{}
func(t Task) Do(){
fmt.Println("击败灭霸,救回队友")
}
type Ironman struct {
Task
}
func(r Ironman) Do(){
fmt.Println("提供成熟的技术方案")
r.Task.Do()
fmt.Println("在战斗中负责打响指,自我牺牲")
}
func main() {
i:=Ironman{ }
i.Do()
}
点击运行
在这场终局之战中,整个联盟的任务就是击败灭霸,救回队友,而钢铁侠的任务除了基本任务外,还负责成熟的技术支持和在战斗中自我牺牲。
总结
本文只是对一些常见的“屏蔽”现象进行简单的说明和演示,让在初学Go语言的过程中对这种现象有一个初步的了解,碰见该现象时知道是怎么回事,从而采取措施避过这种现象或者是从现象中获益。
注:本文所编写的例子均为做演示使用,可能存在逻辑不通或拼写错误的情况,还望见谅。