当前位置: 首页 > 科技观察

关于Go语言,你可能讨厌的五件事

时间:2023-03-21 16:18:11 科技观察

关于Go语言,你有什么可吐槽的?Go在近年来新兴的编程语言中脱颖而出。但称Go为“新人”似乎并不合适,因为Google早在2009年就推出了Go,并在2012年发布了第一个最终版本(Go1.0)。截至目前,Go已经发展到1.10版本,这是一个令人印象深刻的版本,并不断添加新功能。为什么叫eGOtistic……大家都知道Go在实现或者语法上喜欢“我行我素”。在英语中,这种情况被描述为“自以为是”。很多来自其他编程语言的概念在Go中是不存在的,或者即使存在,它们的行为也变得“面目全非”。后一种情况可能会导致意想不到的错误,甚至会使开发人员感到困惑。严格的Go语法常常让开发人员感到疲倦。Go编译器不允许未使用的导入和变量,并尽其所能拦截它们,甚至不在新行上放置花括号。Go强制执行相对固定且几乎统一的编程风格。每当Go编译器不喜欢某些东西时,它最终会变成编译错误。Go提供了非常严格的类型安全。因为它是如此严格,我们甚至可以通过它来实现一些特殊效果和编程错误,其中一些我们将在本文后面讨论。然而,在Go中很少需要显式声明类型,因为类型通常可以通过赋值获得,也就是类型推断。我不是要提供问答!一年多前,我开始在工作中大量使用Go。Go不是我最喜欢的编程语言,但我承认Go在提高开发效率方面起到了作用。事实上,我已经使用Go完成了几个小项目,主要是嵌入式应用程序。GoToolchain的跨平台编译功能(针对其他操作系统或CPU平台编译)非常优秀,已经超越了竞争对手。现在让我们看看Go的一些比较特殊的功能。Go入门实际上很容易,可能只需要一个周末就可以学习基础知识。但是当你开始用Go做更复杂的事情时,各种奇怪的事情开始浮出水面。有时这些属性非常奇怪,以至于Google提供了诸如“为什么X会这样或那样做?”之类的问题的答案。Go的行为在很多方面都与其他语言不同,感觉好像程序员在某些时候注定会陷入某些陷阱。gopherSlack频道已经证实了这种情况的存在,其中有这样的描述:“现在你真的应该好好看看Go,因为每个开发人员在他们的Go职业生涯中都会问这个问题”。很多时候,我们的直觉与Go的特质不符。例如,在Google的C变体中,公共类型、函数、常量等都以大写字母开头表示它们是公共的,而标识符以小写字母开头表示它们是私有的。尽管如此,许多关于Go的决定都是在邮件列表或提案文件中进行长时间讨论的结果,因此得到了确认。然而,所讨论的用例非常具体,以至于许多开发人员仍然不知道这与他们试图解决的问题有何关系。我个人最喜欢的部分是Go不提供可重入锁,即可以由同一线程或Goroutine(Coroutine或GreenThread的变体)递归获取的锁。如果没有黑客,你不能自己做这件事,因为线程在Go中不可用,并且Goroutines不提供可用于递归识别同一个Coroutine的标识符。在这篇文章中,我想介绍Go的五个特性及其语法,这些特性非常晦涩。1.ShadowingMadness让我们从最简单的事情开始:每个优秀的开发人员都听说过Shadowing,它通常发生在变量的上下文中。这是一个只有两个范围的简单示例:foo("foo")funcfoo(var1string){for{var1:="bar"fmt.Println(var1)break}我们使用:=赋值符号创建一个变量,并通过分配的值(类型引用)来推断变量的类型。在这里,它是一个字符串。因此,我们在内部作用域(for循环)中创建一个与函数参数同名的变量。我们隐藏输入参数并输出“bar”。到目前为止,一切都很好。但是在Go中,需要为其他包的属性(即structs、methods、functions等)指定包名,可以在提供Println功能的fmt包中看到。所以让我们对前面的例子做一点重构:foo("foo")funcfoo(var1string){for{fmt:="bar"fmt.Println(var1)break}}这一次,我们得到一个编译错误,我们An试图对字符串调用Println函数。但这并不总是那么明显。当代码突然停止编译时,即使只是几行代码也能给我们一个“惊喜”。如果结构重叠,可能会很麻烦。让我们举一个奇怪的例子:typetaskstruct{}funcmain(){task:=&task{}}我们创建了一个名为task的结构和它的一个实例。我们故意将task小写为结构的名称,因为如前所述,Go使用第一个字母来确定可见性,因此task在这里是私有的。到目前为止它看起来不错,Go编译了我们创建的任务。然而,当我们尝试添加另一行代码时,事情突然发生了变化。typetaskstruct{}funcmain(){task:=&task{}task=&task{}}现在无法编译,表示任务不是类型。在这一点上,Go不知道类型和变量之间的区别。可能有人会说,在JavaScript中,变量task可以是一个类型的引用,但这在Go中是不可能的,因为类型不能作为值赋值给变量。现在的问题是:这是悲剧吗?通常它不会,但它经常在我没有意识到的情况下发生。之后可能还会有一些代码试图访问同名的结构或包,每次都需要几分钟才能找到问题所在。说到类型问题,我们再来看一个例子。2.打字还是不打字,这是个问题!我们已经知道如何创建结构和函数。有时,我们偶尔会“重命名”类型,例如:typehandleint,这将创建一个名为handle的类型,其行为类似于int。通常,此功能称为类型别名。您可能也考虑过此功能,但不是在Go中。不过,从Go1.9开始,这个特性已经得到了全面的支持。让我们看看我们可以用Go做一些很酷的事情:Println(fmt.Sprintf("Iamanint:%d",v))casehandle:fmt.Println(fmt.Sprintf("Iamanhandle:%d",v))}}Iamanint:1Iamanhandle:2在这个例子中我们使用了几个非常Go的很酷的特性。switch-type-case语句是一种模式匹配,类似于Java的instanceof或者JavaScript的typeof。我们将interface{}等同于Java中的Object,因为它是每个Go类自动实现的空接口。有趣的是,Java开发人员希望handle也为int,以便与第一种情况匹配。但事实并非如此,因为OO中的类型继承在Go中不起作用。另一种可能性是handle是int的别名,就像C/C++中的typedef,但也不是这种情况。Go编译器创建了一个新的TypeSpec,可以说是原始类型的克隆。因此,它们彼此完全独立。但是,从Go1.9开始,支持true类型别名。下面的例子只是稍作修改。typehandle=intfuncmain(){varvar1int=1varvar2handle=2types(var1)types(var2)}functypes(valinterface{}){switchv:=val.(type){caseint:fmt.Println(fmt.Sprintf("Iamanint:%d",v))}switchv:=val.(type){casehandle:fmt.Println(fmt.Sprintf("Iamanhandle:%d",v))}}Iamanint:1Iamanint:2Iamanhandle:1Iamanhandle:2你有没有注意到他们的区别?实际上,我们现在不使用typehandleint,而是使用typehandle=int为int创建一个额外的名称(别名),即handle。这意味着switch语句也必须修改,因为此时,int和handle对编译器来说是完全相同的类型,除非你有另一个doublecase,否则你会得到一个编译错误。由于在Go1.9中引入了类型别名,所以很多人会认为上面的类型克隆是类型别名。出于演示目的,让我们定义一个名为Callable的类型,它由一个没有参数和返回值的简单函数组成。typeCallablefunc()现在创建相应的函数。funcmain(){myCallable:=func(){fmt.Println("callable")}test(myCallable)}functest(callableCallable){callable()}看,很简单。由于Go的类型推断机制,编译器自动识别myCallable应该对应Callable的函数签名。因此,编译器能够将myCallable隐式转换为Callable。随后,myCallable被传递给测试函数。这是执行隐式转换的少数例外之一,通常,所有形式的转换都必须显式声明。现在我们已经到了必须使用反射的地步。与其他语言一样,反射提供了在运行时分析或更改行为的能力。类型信息通常用于根据值的数据类型更改运行时行为。typeCallablefunc()funcmain(){callable1:=func(){fmt.Println("callable1")}varcallable2Callablecallable2=func(){fmt.Println("callable2")}test(callable1)test(callable2)}functest(valinterface{}){switchv:=val.(type){casefunc():v()default:fmt.Println("wrongtype")}}callable1wrongtypecallable1现在是函数类型func()并且callable2明确声明为Callable。Callable是一个单独的TypeSpec,因此它与func()不是同一类型。这两种情况现在都必须由我们的反射处理程序分别拦截。然而,这些问题可以通过在Go1.9中引入类型别名来解决。typeCallable=func()3.懒惰是地鼠的天性!Go语言Gopher可爱的logo生性懒惰,所以选择这个logo也有一定的代表意义。我最喜欢的Go特性之一是惰性求值(LazyEvaluation),也就是延迟代码的执行。自从Java引入了StreamAPI之后,Java开发者也知道了这个特性。让我们看一下下面的代码片段:funcmain(){functions:=make([]func(),3)fori:=0;i<3;i++{functions[i]=func(){fmt.Println(fmt.Sprintf("iteratorvalue:%d",i))}}functions[0]()functions[1]()functions[2]()}这里我们有一个包含三个元素的数组,一个循环和一个闭包,结果会怎样?iteratorvalue:3iteratorvalue:3iteratorvalue:3我们会认为是0,1,2,但实际上是3,3,3。这是正确的!而在Java等其他编程语言中,变量的值是在创建闭包时捕获的,而Go只捕获指向变量本身的指针。问题是,在迭代过程中,变量的值不断变化。循环完成后,我们执行闭包,只能看到***的值。我们知道我们只有指针,所以这种行为是可以理解的,但不是很直观。如果我们想保存这个值,我们需要知道在创建闭包时如何计算这个值。funcmain(){functions:=make([]func(),3)fori:=0;i<3;i++{functions[i]=func(yint)func(){returnfunc(){fmt.Println(fmt.Sprintf("iteratorvalue:%d",y))}}(i)}functions[0]()functions[1]()functions[2]()}我们创建一个临时函数,将变量作为参数并返回闭包。我们立即调用这个函数。由于必须在调用外部函数时计算变量的值,因此内部闭包会捕获正确的值。我们得到的是0,1,2。在写这篇文章之前不久,我找到了另一种方法。我们可以在循环内创建一个同名变量,并为其赋值。这也捕获变量的值,因为此方法在循环的每次迭代中创建一个新变量(并因此创建一个新指针)。funcmain(){functions:=make([]func(),3)fori:=0;i<3;i++{i:=i//TrickmitneuerVariablefunctions[i]=func(){fmt.Println(fmt.Sprintf("iteratorvalue:%d",i))}}functions[0]()functions[1]()functions[2]()}就执行速度而言,惰性求值通常是一个有趣的话题。毕竟,我可以在不使用它的情况下创建闭包。在这种情况下,为什么需要这个值?在我看来,这也很不直观。4.我们都是地鼠吗?我们已经知道Go中的interface{}就像Java中的Object-Go中的每个类型都会自动实现这个空接口。然而,接口的自动实现不仅仅适用于空接口,每一个实现了接口所有方法的结构或类型也会自动实现这个接口。为了更好的说明这个问题,我们来看下面的例子:typeSortableinterface{Sort(otherSortable)}定义了这个方法的结构会自动变成Sortable。typeMyStructstruct{}func(mMyStruct)Sort(otherSortable){}除了用于将函数绑定到类型(在本例中为结构)的接收器类型的语法之外,我们还实现了Sortable的所有方法界面。我们现在是可排序的!varsortableSortable=&MyStruct{}自动实现接口乍一看似乎很有用,但它会使事情复杂化,尤其是在大型应用程序中,多个具有相同方法的接口可能会令人困惑没有大脑。开发人员实际想要实现哪个接口?也许他们应该在代码的注释中说清楚!Go也有保证一个类型实现一个接口的方案,就像Java的implements关键字一样,太简单了。typeMyStructstruct{}func(mMyStruct)Sort(otherSortable){}var_Sortable=MyStruct{}var_Sortable=(*MyStruct)(nil)5.nil和nothing现在我们都知道“null”和“nil”之间有很大的区别区别,但也许不是每个人都知道“无”并不总是意味着“无”。为了证明这一点,我们定义了自己的错误类型(异常)。typeMyErrorstringfunc(mMyError)Error()string{returnstring(m)}我们创建了一个从字符串类型克隆而来的新类型。我们只想要一条错误消息,所以这就足够了。要实现错误接口(是的,小写,理论上它不应该公开,但Go可以做到这一切),你必须实现Error方法。接下来,我们需要另一个始终返回Nil的函数。functest(vbool)error{vare*MyError=nilifv{returnil}returne}不管我们传入true还是false,这个函数总是返回nil,是这样吗?c`funcmain(){fmt.Println(nil==test(true))fmt.Println(nil==test(false))}truefalse返回e时,*MyError指针指向接口error的一个实例,即不是零!这符合逻辑吗?当您知道接口在Go中的表示方式时,这是合乎逻辑的。在Go内部,接口是一个包含实际目标实例(此处为nil)和接口类型(此处为error)的结构体,根据Go语言规范,只有当此结构体的两个值为nil时,接口实例为零。所以,如果你真的想返回nil,请明确地这样做。还值得一提的是,如前所述,Go根据名称推断类型和函数的可见性。如果第一个字母是大写字母(例如Foo),则该函数或类型是公共的,如果第一个字母是小写字母(例如foo),则该函数或类型是私有的。但是,在Java中有private,而在Go中只有package-private。一般来说,除了Go中的CamelCase,我们可以使用这个可见性规则,无论是函数、结构体还是常量,但是我们的IDE有语法高亮,所以whocare!有趣的是,Go支持Unicode标识符。因此,Nihongo(Nihongo的意思是日语)是一个完全合法的标识符,但通常被认为是私有的。为什么?因为日语字符没有大写字母。来自“GOSlah”的问候Go在某种程度上是一种非常独特的语言。在日常工作中,您可以尽情享受Go带来的乐趣。如果您已经知道我们在这里提到的陷阱(以及更多),那么即使是开发最大的应用程序也应该没有问题。尽管如此,还是不??断有人提醒说语言有问题。近年来Go发生了很多事情,除了添加新功能外,Go2还列出了许多需要改进的地方,包括语法和运行时行为方面的一些不一致。不过,Go2的推出时间还不得而知,也没有明确的路线图。如果你想使用Go,尽管有陷阱,就使用它。但要为此做好准备:有时您会感到困惑,需要长时间调试,或者通过阅读常见问题解答或访问GopherSlack频道来解决问题。