Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想。
这是一个由计算机领域 “发明之父” 所组成的黄金团队,他们对系统编程语言,操作系统和并行都有着非常深刻的见解。
在 2008 年年中,Go 语言的设计工作接近尾声,一些员工开始以全职工作状态投入到这个项目的编译器和运行实现上。Ian Lance Taylor 也加入到了开发团队中,并于 2008 年 5 月创建了一个 gcc 前端。
Russ Cox 加入开发团队后着手语言和类库方面的开发,也就是 Go 语言的标准包。在 2009 年 10 月 30 日,Rob Pike 以 Google Techtalk 的形式第一次向人们宣告了 Go 语言的存在。
直到 2009 年 11 月 10 日,开发团队将 Go 语言项目以 BSD-style 授权(完全开源)正式公布在 Linux 和 Mac OS X 平台上的版本。Hector Chu 于同年 11 月 22 日公布了 Windows 版本。
作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,并吸引更多的开发者来使用并改善它。自该开源项目发布以来,超过 200 名非谷歌员工的贡献者对 Go 语言核心部分提交了超过 1000 个修改建议。在过去的 18 个月里,又有 150 开发者贡献了新的核心代码。这俨然形成了世界上最大的开源团队,并使该项目跻身 Ohloh 前 2% 的行列。大约在 2011 年 4 月 10 日,谷歌开始抽调员工进入全职开发 Go 语言项目。开源化的语言显然能够让更多的开发者参与其中并加速它的发展速度。Andrew Gerrand 在 2010 年加入到开发团队中成为共同开发者与支持者。
在 Go 语言在 2010 年 1 月 8 日被 Tiobe(闻名于它的编程语言流行程度排名)宣布为 “2009 年年度语言” 后,引起各界很大的反响。目前 Go 语言在这项排名中的最高记录是在 2010 年 2 月创下的第13名,流行程度 1778%。
时间轴:
- 2007 年 9 月 21 日:雏形设计
- 2009 年 11 月 10日:首次公开发布
- 2010 年 1 月 8 日:当选 2009 年年度语言
- 2010 年 5 月:谷歌投入使用
- 2011 年 5 月 5 日:Google App Engine 支持 Go 语言
从 2010 年 5 月起,谷歌开始将 Go 语言投入到后端基础设施的实际开发中,例如开发用于管理后端复杂环境的项目。有句话叫 “吃你自己的狗食”,这也体现了谷歌确实想要投资这门语言,并认为它是有生产价值的。
Go 语言的官方网站是 golang.org,这个站点采用 Python 作为前端,并且使用 Go 语言自带的工具 godoc 运行在 Google App Engine 上来作为 Web 服务器提供文本内容。在官网的首页有一个功能叫做 Go Playground,是一个 Go 代码的简单编辑器的沙盒,它可以在没有安装 Go 语言的情况下在你的浏览器中编译并运行 Go,它提供了一些示例,其中包括国际惯例 “Hello, World!”。
更多的信息详见 github.com/golang/go,Go 项目 Bug 追踪和功能预期详见 github.com/golang/go/issues。
Go 通过以下的 Logo 来展示它的速度,并以囊地鼠(Gopher)作为它的吉祥物。
谷歌邮件列表 golang-nuts 非常活跃,每天的讨论和问题解答数以百计。
关于 Go 语言在 Google App Engine 的应用,这里有一个单独的邮件列表 google-appengine-go,不过 2 个邮件列表的讨论内容并不是分得很清楚,都会涉及到相关的话题。go-lang.cat-v.org/ 是 Go 语言开发社区的资源站,irc.freenode.net 的#go-nuts 是官方的 Go IRC 频道。
@golang 是 Go 语言在 Twitter 的官方帐号,大家一般使用 #golang 作为话题标签。
这里还有一个在 Linked-in 的小组:www.linkedin.com/groups?gid=2524765&trk=myg_ugrp_ovr。
Go 编程语言的维基百科:en.wikipedia.org/wiki/Go_(programming_language)
Go 语言相关资源的搜索引擎页面:gowalker.org
Go 语言还有一个运行在 Google App Engine 上的 Go Tour,你也可以通过执行命令 go install go-tour.googlecode.com/hg/gotour
安装到你的本地机器上。可以访问该指南的 中文版本,或通过命令 go install https://bitbucket.org/mikespook/go-tour-zh/gotour
进行安装。
正如 “21 世纪的 C 语言” 这句话所说,Go 语言并不是凭空而造的,而是和 C++、Java 和 C# 一样属于 C 系。不仅如此,设计者们还汲取了其它编程语言的精粹部分融入到 Go 语言当中。
在声明和包的设计方面,Go 语言受到 Pascal、Modula 和 Oberon 系语言的影响;在并发原理的设计上,Go 语言从同样受到 Tony Hoare 的 CSP(通信序列进程 Communicating Squential Processes)理论影响的 Limbo 和 Newsqueak 的实践中借鉴了一些经验,并使用了和 Erlang 类似的机制。
这是一门完全开源的编程语言,因为它使用 BSD 授权许可,所以任何人都可以进行商业软件的开发而不需要支付任何费用。
尽管为了能够让目前主流的开发者们能够对 Go 语言中的类 C 语言的语法感到非常亲切而易于转型,但是它在极大程度上简化了这些语法,使得它们比 C/C++ 的语法更加简洁和干净。同时,Go 语言也拥有一些动态语言的特性,这使得使用 Python 和 Ruby 的开发者们在使用 Go 语言的时候感觉非常容易上手。
下图展示了一些其它编程语言对 Go 语言的影响:
为什么要创造一门编程语言:
- C/C++ 的发展速度无法跟上计算机发展的脚步,十多年来也没有出现一门与时代相符的主流系统编程语言,因此人们需要一门新的系统编程语言来弥补这个空缺,尤其是在计算机信息时代。
- 对比计算机性能的提升,软件开发领域不被认为发展地足够快或者比硬件发展更加成功(有许多项目均以失败告终),同时应用程序的体积始终在不断地扩大,这就迫切地需要一门具备更高层次概念的低级语言来突破现状。
- 在 Go 语言出现之前,开发者们总是面临非常艰难的抉择,究竟是使用执行速度快但是编译速度并不理想的语言(如:C++),还是使用编译速度较快但执行效率不佳的语言(如:.NET、Java),或者说开发难度较低但执行速度一般的动态语言呢?显然,Go 语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发。
1)Go 语言的发展目标
Go 语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行。
因此,Go 语言是一门类型安全和内存安全的编程语言。虽然 Go 语言中仍有指针的存在,但并不允许进行指针运算。
Go 语言的另一个目标是对于网络通信、并发和并行编程的极佳支持,从而更好地利用大量的分布式和多核的计算机,这一点对于谷歌内部的使用来说就非常重要了。设计者通过 goroutine 这种轻量级线程的概念来实现这个目标,然后通过 channel 来实现各个 goroutine 之间的通信。他们实现了分段栈增长和 goroutine 在线程基础上多路复用技术的自动化。
这个特性显然是 Go 语言最强有力的部分,不仅支持了日益重要的多核与多处理器计算机,也弥补了现存编程语言在这方面所存在的不足。
Go 语言中另一个非常重要的特性就是它的构建速度(编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒。作为大量使用 C++ 来构建基础设施的谷歌来说,无疑从根本上摆脱了 C++ 在构建速度上非常不理想的噩梦。这不仅极大地提升了开发者的生产力,同时也使得软件开发过程中的代码测试环节更加紧凑,而不必浪费大量的时间在等待程序的构建上。
依赖管理是现今软件开发的一个重要组成部分,但是 C 语言中“头文件”的概念却导致越来越多因为依赖关系而使得构建一个大型的项目需要长达几个小时的时间。人们越来越需要一门具有严格的、简洁的依赖关系分析系统从而能够快速编译的编程语言。这正是 Go 语言采用包模型的根本原因,这个模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性。
整个 Go 语言标准库的编译时间一般都在 20 秒以内,其它的常规项目也只需要半秒钟的时间来完成编译工作。这种闪电般的编译速度甚至比编译 C 语言或者 Fortran 更加快,使得编译这一环节不再成为在软件开发中困扰开发人员的问题。在这之前,动态语言将快速编译作为自身的一大亮点,像 C++ 那样的静态语言一般都有非常漫长的编译和链接工作。而同样作为静态语言的 Go 语言,通过自身优良的构建机制,成功地了去除了这个弊端,使得程序的构建过程变得微不足道,拥有了像脚本语言和动态语言那样的高效开发的能力。
另外,Go 语言在执行速度方面也可以与 C/C++ 相提并论。
由于内存问题(通常称为内存泄漏)长期以来一直伴随着 C++ 的开发者们,Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记-清除算法)。
尽管垃圾回收并不容易实现,但考虑这将是未来并发应用程序发展的一个重要组成部分,Go 语言的设计者们还是完成了这项艰难的任务。
Go 语言还能够在运行时进行反射相关的操作。
使用 go install
能够很轻松地对第三方包进行部署。
此外,Go 语言还支持调用由 C 语言编写的海量库文件,从而能够将过去开发的软件进行快速迁移。
2)指导设计原则
Go语言通过减少关键字的数量(25 个)来简化编码过程中的混乱和复杂度。干净、整齐和简洁的语法也能够提高程序的编译速度,因为这些关键字在编译过程中少到甚至不需要符号表来协助解析。
这些方面的工作都是为了减少编码的工作量,甚至可以与 Java 的简化程度相比较。
Go 语言有一种极简抽象艺术家的感觉,因为它只提供了一到两种方法来解决某个问题,这使得开发者们的代码都非常容易阅读和理解。众所周知,代码的可读性是软件工程里最重要的一部分( 代码是写给人看的,不是写给机器看的)。
这些设计理念没有建立其它概念之上,所以并不会因为牵扯到一些概念而将某个概念复杂化,他们之间是相互独立的。
Go 语言有一套完整的编码规范,你可以在 Go 语言编码规范 页面进行查看。
它不像 Ruby 那样通过实现过程来定义编码规范。作为一门具有明确编码规范的语言,它要求可以采用不同的编译器如 gc 和 gccgo 进行编译工作,这对语言本身拥有更好的编码规范起到很大帮助。
LALR 是 Go 语言的语法标准,你也可以在 src/cmd/internal/gc/go.y 中查看到,这种语法标准在编译时不需要符号表来协助解析。
3)语言的特性
Go 语言从本质上(程序和结构方面)来实现并发编程。
因为 Go 语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go 语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说这是一门混合型的语言。
在传统的面向对象语言中,使用面向对象编程技术显得非常的臃肿,它们总是通过复杂的模式来构建庞大的类型层级,这违背了编程语言应该提升生产力的宗旨。
函数是 Go 语言中的基本构件,它们的使用方法非常灵活。
Go 语言使用静态类型,所以它是类型安全的一门语言,加上通过构建到本地代码,程序的执行速度也非常快。
作为强类型语言,隐式的类型转换是不被允许的,记住一条原则:让所有的东西都是显式的。
Go 语言其实也有一些动态语言的特性(通过关键字 var
),所以它对那些逃离 Java 和 .Net 世界而使用 Python、Ruby、PHP 和 JavaScript 的开发者们也具有很大的吸引力。
Go 语言支持交叉编译,比如说你可以在运行 Linux 系统的计算机上开发运行下 Windows 下运行的应用程序。这是第一门完全支持 UTF-8 的编程语言,这不仅体现在它可以处理使用 UTF-8 编码的字符串,就连它的源码文件格式都是使用的 UTF-8 编码。Go 语言做到了真正的国际化!
4)Go 性能说明
根据 Go 开发团队和基本的算法测试,Go 语言与 C 语言的性能差距大概在 10%~20% 之间。虽然没有官方的性能标准,但是与其它各个语言相比已经拥有非常出色的表现。
如果说 Go 语言的执行效率大约比 C++ 慢 20% 也许更有实际意义。保守估计在相同的环境和执行目标的情况下,Go 程序比 Java 或 Scala 应用程序要快上 2 倍,并比这两门语言使用少占用 70% 的内存。在很多情况下这种比较是没有意义的,因为像谷歌这样拥有成千上万台服务器的公司都抛弃 C++ 而开始将 Go 用于生产环境已经足够说明它本身所具有的优势。
时下流行的语言大都是运行在虚拟机上,如:Java 和 Scala 使用的 JVM,C# 和 VB.NET 使用的 .NET CLR。尽管虚拟机的性能已经有了很大的提升,但任何使用 JIT 编译器和脚本语言解释器的编程语言(Ruby、Python、Perl 和 JavaScript)在 C 和 C++ 的绝对优势下甚至都无法在性能上望其项背。
如果说 Go 比 C++ 要慢 20%,那么 Go 就要比任何非静态和编译型语言快 2 到 10 倍,并且能够更加高效地使用内存。
其实比较多门语言之间的性能是一种非常猥琐的行为,因为任何一种语言都有其所擅长和薄弱的方面。例如在处理文本方面,那些只处理纯字节的语言显然要比处理 Unicode 这种更为复杂编码的语言要出色的多。有些人可能认为使用两种不同的语言实现同一个目标能够得出正确的结论,但是很多时候测试者可能对一门语言非常了解而对另一门语言只是大概明白,测试者对程序编写的手法在一定程度也会影响结果的公平性,因此测试程序应该分别由各自语言的擅长者来编写,这样才能得到具有可比性的结果。另外,像在统计学方面,人们很难避免人为因素对结果的影响,所以这在严格意义上并不是科学。还要注意的是,测试结果的可比性还要根据测试目标来区别,例如很多发展十多年的语言已经针对各类问题拥有非常成熟的类库,而作为一门新生语言的 Go 语言,并没有足够的时间来推导各类问题的最佳解决方案。如果你想获取更多有关性能的资料,请访问 Computer Language Benchmark Game
这里有一些评测结果:
-
比较 Go 和 Python 在简单的 web 服务器方面的性能,单位为传输量每秒:
原生的 Go http 包要比 web.py 快 7 至 8 倍,如果使用 web.go 框架则稍微差点,比 web.py 快 6 至 7 倍。在 Python 中被广泛使用的 tornado 异步服务器和框架在 web 环境下要比 web.py 快很多,Go 大概只比它快 1.2 至 1.5 倍。
-
Go 和 Python 在一般开发的平均水平测试中,Go 要比 Python 3 快 25 倍左右,少占用三分之二的内存,但比 Python 大概多写一倍的代码(详见引用 27)。
-
根据 Robert Hundt的文章对 C++、Java、Go 和 Scala,以及 Go 开发团队的反应,可以得出以下结论:
- Go 和 Scala 之间具有更多的可比性(都使用更少的代码),而 C++ 和 Java 都使用非常冗长的代码。
- Go 的编译速度要比绝大多数语言都要快,比 Java 和 C++ 快 5 至 6 倍,比 Scala 快 10 倍。
- Go 的二进制文件体积是最大的(每个可执行文件都包含 runtime)。
- 在最理想的情况下,Go 能够和 C++ 一样快,比 Scala 快 2 至 3 倍,比 Java 快 5 至 10 倍。
- Go 在内存管理方面也可以和 C++ 相媲美,几乎只需要 Scala 所使用的一半,比 Java 少 4 倍左右。
5)语言的用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
Go 语言一个非常好的目标就是实现所谓的复杂事件处理(CEP),这项技术要求海量并行支持,高度的抽象化和高性能。当我们进入到物联网时代,CEP 必然会成为人们关注的焦点。
但是 Go 语言同时也是一门可以用于实现一般目标的语言,例如对于文本的处理,前端展现,甚至像使用脚本一样使用它。
值得注意的是,因为垃圾回收和自动内存分配的原因,Go 语言不适合用来开发对实时性要求很高的软件。
越来越多的谷歌内部的大型分布式应用程序都开始使用 Go 语言来开发,例如谷歌地球的一部分代码就是由 Go 语言完成的。
如果你想知道一些其它组织使用Go语言开发的实际应用项目,你可以到 使用 Go 的组织 页面进行查看。出于隐私保护的考虑,许多公司的项目都没有展示在这个页面。
在 Chrome 浏览器中内置了一款 Go 语言的编译器用于本地客户端(NaCl),这很可能会被用于在 Chrome OS 中执行 Go 语言开发的应用程序。
Go 语言可以在 Intel 或 ARM 处理器上运行,因此它也可以在安卓系统下运行,例如 Nexus 系列的产品。
在 Google App Engine 中使用 Go 语言:2011 年 5 月 5 日,官方发布了用于开发运行在 Google App Engine 上的 Web 应用的 Go SDK,在此之前,开发者们只能选择使用 Python 或者 Java。这主要是 David Symonds 和 Nigel Tao 努力的成果。目前最新的稳定版是基于 Go 1.4 的 SDK 1.9.18,于 2015 年 2 月 18 日发布。当前 Go 语言的稳定版本是 Go 1.4.2。
6)关于特性缺失
许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持,但其中的一部分可能会在未来被支持。
- 为了简化设计,不支持函数重载和操作符重载
- 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
- Go 语言通过另一种途径实现面向对象设计来放弃类和类型的继承
- 尽管在接口的使用方面可以实现类似变体类型的功能,但本身不支持变体类型
- 不支持动态加载代码
- 不支持动态链接库
- 不支持泛型
- 通过
recover
和panic
来替代异常机制 - 不支持断言
- 不支持静态变量
关于 Go 语言开发团队对于这些方面的讨论,你可以通过 Go 常见问题 页面查看。
7)使用 Go 语言编程
如果你有其它语言的编程经历(面向对象编程语言,如:Java、C#、Object-C、Python、Ruby),在你进入到 Go 语言的世界之后,你将会像迷恋你的 X 语言一样无法自拔。Go 语言使用了与其它语言不同的设计模式,所以当你尝试将你的X语言的代码迁移到 Go 语言时,你将会非常失望,所以你需要从头开始,用 Go 的理念来思考。
如果你在至高点使用 Go 的理念来重新审视和分析一个问题,你通常会找到一个适用于 Go 语言的优雅的解决方案。
8)与其它语言进行交互
与 C 进行交互
工具 cgo 提供了对 FFI(外部函数接口)的支持,能够使用 Go 代码安全地调用 C 语言库,你可以访问 cgo 文档主页:http://golang.org/cmd/cgo。cgo 会替代 Go 编译器来产生可以组合在同一个包中的 Go 和 C 代码。在实际开发中一般使用 cgo 创建单独的 C 代码包。
如果你想要在你的 Go 程序中使用 cgo,则必须在单独的一行使用 import "C" 来导入,一般来说你可能还需要 import "unsafe"。
然后,你可以在 import "C" 之前使用注释(单行或多行注释均可)的形式导入 C 语言库(甚至有效的 C 语言代码),它们之间没有空行,例如:
// #include
// #include
import "C"
名称 "C" 并不属于标准库的一部分,这只是 cgo 集成的一个特殊名称用于引用 C 的命名空间。在这个命名空间里所包含的 C 类型都可以被使用,例如 C.uint、C.long 等等,还有 libc 中的函数 C.random() 等也可以被调用。
当你想要使用某个类型作为 C 中函数的参数时,必须将其转换为 C 中的类型,反之亦然,例如:
var i int
C.uint(i) // 从 Go 中的 int 转换为 C 中的无符号 int
int(C.random()) // 从 C 中 random() 函数返回的 long 转换为 Go 中的 int
下面的 2 个 Go 函数 Random() 和 Seed() 分别调用了 C 中的 C.random() 和 C.srandom()。
示例 c1.go
package rand
// #include
import "C"
func Random() int {
return int(C.random())
}
func Seed(i int) {
C.srandom(C.uint(i))
}
C 当中并没有明确的字符串类型,如果你想要将一个 string 类型的变量从 Go 转换到 C 时,可以使用 C.CString(s);同样,可以使用 C.GoString(cs) 从 C 转换到 Go 中的 string 类型。
Go 的内存管理机制无法管理通过 C 代码分配的内存。
开发人员需要通过手动调用 C.free
来释放变量的内存:
defer C.free(unsafe.Pointer(Cvariable))
这一行最好紧跟在使用 C 代码创建某个变量之后,这样就不会忘记释放内存了。下面的代码展示了如何使用 cgo 创建变量、使用并释放其内存:
示例 c2.go
package print
// #include
// #include
import "C"
import "unsafe"
func Print(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.fputs(cs, (*C.FILE)(C.stdout))
}
构建 cgo 包
Makefile 文件(因为我们使用了一个独立的包),除了使用变量 GOFILES 之外,还需要使用变量 CGOFILES 来列出需要使用 cgo 编译的文件列表。例如,上面示例中的代码就可以使用包含以下内容的 Makefile 文件来编译,你可以使用 gomake 或 make:
include $(GOROOT)/src/Make.inc
TARG=rand
CGOFILES=\
c1.go\
include $(GOROOT)/src/Make.pkg
与 C++ 进行交互
SWIG(简化封装器和接口生成器)支持在 Linux 系统下使用 Go 代码调用 C 或者 C++ 代码。这里有一些使用 SWIG 的注意事项:
- 编写需要封装的库的 SWIG 接口。
- SWIG 会产生 C 的存根函数。
- 这些库可以使用 cgo 来调用。
- 相关的 Go 文件也可以被自动生成。
这类接口支持方法重载、多重继承以及使用 Go 代码实现 C++ 的抽象类。
目前使用 SWIG 存在的一个问题是它无法支持所有的 C++ 库,比如说它就无法解析 TObject.h。
总结:
这里列举一些 Go 语言的必杀技:
- 简化问题,易于学习
- 内存管理,简洁语法,易于使用
- 快速编译,高效开发
- 高效执行
- 并发支持,轻松驾驭
- 静态类型
- 标准类库,规范统一
- 易于部署
- 文档全面
- 免费开源
Go 语言开发团队开发了适用于以下操作系统的编译器:
- Linux
- FreeBSD
- Mac OS X(也称为 Darwin)
目前有2个版本的编译器:Go 原生编译器 gc 和非原生编译器 gccgo,这两款编译器都是在类 Unix 系统下工作 。其中,gc 版本的编译器已经被移植到 Windows 平台上,并集成在主要发行版中,你也可以通过安装 MinGW 从而在 Windows 平台下使用 gcc 编译器。这两个编译器都是以单通道的形式工作。
你可以获取以下平台上的 Go 1.4 源码和二进制文件:
- Linux 2.6+:amd64、386 和 arm 架构
- Mac OS X(Snow Leopard + Lion):amd64 和 386 架构
- Windows 2000+:amd64 和 386 架构
对于非常底层的纯 Go 语言代码或者包而言,在各个操作系统平台上的可移植性是非常强的,只需要将源码拷贝到相应平台上进行编译即可,或者可以使用交叉编译来构建目标平台的应用程序。但如果你打算使用 cgo 或者类似文件监控系统的软件,就需要根据实际情况进行相应地修改了。
- Go 原生编译器 gc
主要基于 Ken Thompson 先前在 Plan 9 操作系统上使用的 C 工具链。
Go 语言的编译器和链接器都是使用 C 语言编写并产生本地代码,Go 不存在自我引导之类的功能。因此如果使用一个有不同指令集的编译器来构建 Go 程序,就需要针对操作系统和处理器架构(32 位操作系统或 64 位操作系统)进行区别对待。
这款编译器使用非分代、无压缩和并行的方式进行编译,它的编译速度要比 gccgo 更快,产生更好的本地代码,但编译后的程序不能够使用 gcc 进行链接。
编译器目前支持以下基于 Intel 或 AMD 处理器架构的程序构建。
gc 编译器支持的处理器架构:
当你第一次看到这套命名系统的时候你会觉得很奇葩,不过这些命名都是来自于 Plan 9 项目。
- g = 编译器:将源代码编译为项目代码(程序文本)
- l = 链接器:将项目代码链接到可执行的二进制文件(机器代码)
(相关的 C 编译器名称为 6c、8c 和 5c,相关的汇编器名称为 6a、8a 和 5a)
标记(Flags) 是指可以通过命令行设置可选参数来影响编译器或链接器的构建过程或得到一个特殊的目标结果。
可用的编译器标记如下:
flags:
-I 针对包的目录搜索
-d 打印声明信息
-e 不限制错误打印的个数
-f 打印栈结构
-h 发生错误时进入恐慌(panic)状态
-o 指定输出文件名
-S 打印产生的汇编代码
-V 打印编译器版本
-u 禁止使用 unsafe 包中的代码
-w 打印归类后的语法解析树
-x 打印 lex tokens
从 Go 1.0.3 版本开始,不再使用 8g,8l 之类的指令进行程序的构建,取而代之的是统一的 go build 和 go install 等命令,而这些指令会自动调用相关的编译器或链接器。
如果你想获得更深层次的信息,你可以在目录 $GOROOT/src/cmd 下找到编译器和链接器的源代码。Go 语言本身是由 C 语言开发的,而不是 Go 语言(Go 1.5 开始自举)。词法分析程序是 GNU bison,语法分析程序是名为$GOROOT/src/cmd/gc/go.y 的 yacc 文件,它会在同一目录输出 y.tab.{c,h}
文件。如果你想知道更多有关构建过程的信息,你可以在 $GOROOT/src/make.bash 中找到。
大部分的目录都包含了名为 doc.go 的文件,这个文件提供了更多详细的信息。
- gccgo 编译器
一款相对于 gc 而言更加传统的编译器,使用 GCC 作为后端。GCC 是一款非常流行的 GNU 编译器,它能够构建基于众多处理器架构的应用程序。编译速度相对 gc 较慢,但产生的本地代码运行要稍微快一点。它同时也提供一些与 C 语言之间的互操作性。
从 Go 1 版本开始,gc 和 gccgo 在编译方面都有等价的功能。
- 文件扩展名与包(package)
Go 语言源文件的扩展名很显然就是 .go。
C 文件使用后缀名 .c,汇编文件使用后缀名 .s。所有的源代码文件都是通过包(packages)来组织。包含可执行代码的包文件在被压缩后使用扩展名 .a(AR 文档)。
Go 语言的标准库,包文件在被安装后就是使用这种格式的文件。
注意:当你在创建目录时,文件夹名称永远不应该包含空格,而应该使用下划线 "_" 或者其它一般符号代替。
2、Go 环境变量Go 开发环境依赖于一些操作系统环境变量,你最好在安装 Go 之间就已经设置好他们。如果你使用的是 Windows 的话,你完全不用进行手动设置,Go 将被默认安装在目录 c:/go
下。这里列举几个最为重要的环境变量:
- $GOROOT 表示 Go 在你的电脑上的安装位置,它的值一般都是
$HOME/go
,当然,你也可以安装在别的地方。 - $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。
- $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
- $GOBIN 表示编译器和链接器的安装位置,默认是
$GOROOT/bin
,如果你使用的是 Go 1.0.3 及以后的版本,一般情况下你可以将它的值设置为空,Go 将会使用前面提到的默认值。
目标机器是指你打算运行你的 Go 应用程序的机器。
Go 编译器支持交叉编译,也就是说你可以在一台机器上构建运行在具有不同操作系统和处理器架构上运行的应用程序,也就是说编写源代码的机器可以和目标机器有完全不同的特性(操作系统与处理器架构)。
为了区分本地机器和目标机器,你可以使用 $GOHOSTOS 和 $GOHOSTARCH 设置目标机器的参数,这两个变量只有在进行交叉编译的时候才会用到,如果你不进行显示设置,他们的值会和本地机器($GOOS 和 $GOARCH)一样。
- $GOPATH 默认采用和
$GOROOT
一样的值,但从 Go 1.1 版本开始,你必须修改为其它路径。它可以包含多个包含 Go 语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:src
、pkg
和bin
,这三个目录分别用于存放源码文件、包文件和可执行文件。 - $GOARM 专门针对基于 arm 架构的处理器,它的值可以是 5 或 6,默认为 6。
- $GOMAXPROCS 用于设置应用程序可使用的处理器个数与核数。
在接下来的章节中,我们将会讨论如何在 Linux、Mac OS X 和 Windows 上安装 Go 语言。在 FreeBSD 上的安装和 Linux 非常类似。开发团队正在尝试将 Go 语言移植到其它例如 OpenBSD、DragonFlyBSD、NetBSD、Plan 9、Haiku 和 Solaris 操作系统上,你可以在这个页面找到最近的动态:Go Porting Efforts。
3、在 Linux 上安装 Go如果你能够自己下载并编译 Go 的源代码来说是非常有教育意义的,你可以根据这个页面找到安装指南和下载地址:Download the Go distribution。
我们接下来也会带你一步步的完成安装过程。
1)设置 Go 环境变量
我们在 Linux 系统下一般通过文件 $HOME/.bashrc 配置自定义环境变量,根据不同的发行版也可能是文件$HOME/.profile,然后使用 gedit 或 vi 来编辑文件内容。
export GOROOT=$HOME/go
为了确保相关文件在文件系统的任何地方都能被调用,你还需要添加以下内容:
export PATH=$PATH:$GOROOT/bin
在开发 Go 项目时,你还需要一个环境变量来保存你的工作目录。
export GOPATH=$HOME/Applications/Go
$GOPATH 可以包含多个工作目录,取决于你的个人情况。如果你设置了多个工作目录,那么当你在之后使用 go get(远程包安装命令)时远程包将会被安装在第一个目录下。
在完成这些设置后,你需要在终端输入指令 source .bashrc 以使这些环境变量生效。然后重启终端,输入 go env和 env 来检查环境变量是否设置正确。
2)安装 C 工具
Go 的工具链是用 C 语言编写的,因此在安装 Go 之前你需要先安装相关的 C 工具。如果你使用的是 Ubuntu 的话,你可以在终端输入以下指令( 由于网络环境的特殊性,你可能需要将每个工具分开安装 )。
sudo apt-get install bison ed gawk gcc libc6-dev make
你可以在其它发行版上使用 RPM 之类的工具。
3)获取 Go 源代码
从 官方页面 或 国内镜像 下载 Go 的源码包到你的计算机上,然后将解压后的目录 go
通过命令移动到 $GOROOT
所指向的位置。
wget https://storage.googleapis.com/golang/go.src.tar.gz
tar zxv go.src.tar.gz
sudo mv go $GOROOT
4)构建 Go
在终端使用以下指令来进行编译工作。
cd $GOROOT/src
./all.bash
在完成编译之后(通常在 1 分钟以内,如果你在 B 型树莓派上编译,一般需要 1 个小时),你会在终端看到如下信息被打印:
注意事项:
在测试 net/http 包时有一个测试会尝试连接 google.com,你可能会看到如下所示的一个无厘头的错误报告:
‘make[2]: Leaving directory `/localusr/go/src/pkg/net’
如果你正在使用一个带有防火墙的机器,我建议你可以在编译过程中暂时关闭防火墙,以避免不必要的错误。
解决这个问题的另一个办法是通过设置环境变量 $DISABLE_NET_TESTS 来告诉构建工具忽略 net/http 包的相关测试:
export DISABLE_NET_TESTS=1
如果你完全不想运行包的测试,你可以直接运行 ./make.bash 来进行单纯的构建过程。
5)测试安装
使用你最喜爱的编辑器来输入以下内容,并保存为文件名 test.go。
示例 hello_world1.go
package main
func main() {
println("Hello", "world")
}
切换相关目录到下,然后执行指令 go run hello_world1.go,将会打印信息:Hello, world。
6)验证安装版本
你可以通过在终端输入指令 go version 来打印 Go 的版本信息。
如果你想要通过 Go 代码在运行时检测版本,可以通过以下例子实现。
示例 2.2 version.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("%s", runtime.Version())
}
这段代码将会输出 go1.4.2 或类似字符串。
7)更新版本
你可以在 发布历史 页面查看到最新的稳定版。
Go 的源代码有以下三个分支:
- Go release:最新稳定版,实际开发最佳选择
- Go weekly:包含最近更新的版本,一般每周更新一次
- Go tip:永远保持最新的版本,相当于内测版
当你在使用不同的版本时,注意官方博客发布的信息,因为你所查阅的文档可能和你正在使用的版本不相符。
4、在 Mac OS X 上安装 Go
如果你想要在你的 Mac 系统上安装 Go,则必须使用 Intel 64 位处理器,Go 不支持 PowerPC 处理器。
你可以通过该页面查看有关在 PowerPC 处理器上的移植进度:https://codedr-go-ppc.googlecode.com/hg/。
注意事项:
在 Mac 系统下使用到的 C 工具链是 Xcode 的一部分,因此你需要通过安装 Xcode 来完成这些工具的安装。你并不需要安装完整的 Xcode,而只需要安装它的命令行工具部分。
你可以在 下载页面 页面下载到 Mac 系统下的一键安装包或源代码自行编译。
通过源代码编译安装的过程与环境变量的配置与在 Linux 系统非常相似,因此不再赘述。
5、在 Windows 上安装 Go你可以在 下载页面 页面下载到 Windows 系统下的一键安装包。
前期的 Windows 移植工作由 Hector Chu 完成,但目前的发行版已经由 Joe Poirier 全职维护。
在完成安装包的安装之后,你只需要配置 $GOPATH 这一个环境变量就可以开始使用 Go 语言进行开发了,其它的环境变量安装包均会进行自动设置。在默认情况下,Go 将会被安装在目录 c:\go 下,但如果你在安装过程中修改安装目录,则可能需要手动修改所有的环境变量的值。
如果你想要测试安装,则可以使用指令 go run
运行 hello_world1.go。
如果发生错误 fatal error: can’t find import: fmt 则说明你的环境变量没有配置正确。
如果你想要在 Windows 下使用 cgo (调用 C 语言写的代码),则需要安装 MinGW,一般推荐安装 TDM-GCC。如果你使用的是 64 位操作系统,请务必安装 64 位版本的 MinGW。安装完成进行环境变量等相关配置即可使用。
在 Windows 下运行在虚拟机里的 Linux 系统上安装 Go:
如果你想要在 Windows 下的虚拟机里的 Linux 系统上安装 Go,你可以选择使用虚拟机软件 VMware,下载 VMware player,搜索并下载一个你喜欢的 Linux 发行版镜像,然后安装到虚拟机里,安装 Go 的流程请参考前文。
6、安装目录清单你的 Go 安装目录($GOROOT)的文件夹结构应该如下所示:
README.md, AUTHORS, CONTRIBUTORS, LICENSE
/bin
:包含可执行文件,如:编译器,Go 工具/doc
:包含示例程序,代码工具,本地文档等/lib
:包含文档模版/misc
:包含与支持 Go 编辑器有关的配置文件以及 cgo 的示例/os_arch
:包含标准库的包的对象文件(.a
)/src
:包含源代码构建脚本和标准库的包的完整源代码(Go 是一门开源语言)/src/cmd
:包含 Go 和 C 的编译器和命令行脚本
尽管 Go 编译器产生的是本地可执行代码,这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime 包中找到)当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。
runtime 主要由 C 语言编写(Go 1.5 开始自举),并且是每个 Go 包的最顶级包。你可以在目录 $GOROOT/src/runtime 中找到相关内容。
垃圾回收器:Go 拥有简单却高效的标记-清除回收器。它的主要思想来源于 IBM 的可复用垃圾回收器,旨在打造一个高效、低延迟的并发回收器。目前 gccgo 还没有回收器,同时适用 gc 和 gccgo 的新回收器正在研发中。使用一门具有垃圾回收功能的编程语言不代表你可以避免内存分配所带来的问题,分配和回收内容都是消耗 CPU 资源的一种行为。
Go 的可执行文件都比相对应的源代码文件要大很多,这恰恰说明了 Go 的 runtime 嵌入到了每一个可执行文件当中。当然,在部署到数量巨大的集群时,较大的文件体积也是比较头疼的问题。但总得来说,Go 的部署工作还是要比 Java 和 Python 轻松得多。因为 Go 不需要依赖任何其它文件,它只需要一个单独的静态文件,这样你也不会像使用其它语言一样在各种不同版本的依赖文件之间混淆。
8、Go 解释器因为 Go 具有像动态语言那样快速编译的能力,自然而然地就有人会问 Go 语言能否在 REPL(read-eval-print loop)编程环境下实现。
Sebastien Binet 已经使用这种环境实现了一个 Go 解释器,你可以在这个页面找到:GitHub - sbinet/igo: A simple interactive Go interpreter built on go-eval with some readline-like refinements
9、编辑器与集成开发环境因为 Go 语言还是一门相对年轻的编程语言,所以不管是在集成开发环境(IDE)还是相关的插件方面,发展都不是很成熟。不过目前还是有一些 IDE 能够较好地支持 Go 的开发,有些开发工具甚至是跨平台的,你可以在 Linux、Mac OS X 或者 Windows 下工作。
你可以通过查阅 编辑器和 IDE 扩展 页面来获取 Go 开发工具的最新信息。
1)Go 开发环境的基本要求
这里有一个你可以期待你用来开发 Go 的集成开发环境有哪些特性的列表,从而替代你使用文本编辑器写代码和命令行编译与链接程序的方式。
- 语法高亮是必不可少的功能,这也是为什么每个开发工具都提供配置文件来实现自定义配置的原因。
- 可以自动保存代码,至少在每次编译前都会保存。
- 可以显示代码所在的行数。
- 拥有较好的项目文件纵览和导航能力,可以同时编辑多个源文件并设置书签,能够匹配括号,能够跳转到某个函数或类型的定义部分。
- 完美的查找和替换功能,替换之前最好还能预览结果。
- 可以注释或取消注释选中的一行或多行代码。
- 当有编译错误时,双击错误提示可以跳转到发生错误的位置。
- 跨平台,能够在 Linux、Mac OS X 和 Windwos 下工作,这样就可以专注于一个开发环境。
- 最好是免费的,不过有些开发者还是希望能够通过支付一定金额以获得更好的开发环境。
- 最好是开源的。
- 能够通过插件架构来轻易扩展和替换某个功能。
- 尽管集成开发环境本身就是非常复杂的,但一定要让人感觉操作方便。
- 能够通过代码模版来简化编码过程从而提升编码速度。
- 使用 Go 项目的概念来浏览和管理项目中的文件,同时还要拥有构建系统的概念,这样才能更加方便的构建、清理或运行我们建立的程序或项目。构建出的程序需要能够通过命令行或 IDE 内部的控制台运行。
- 拥有断点、检查变量值、单步执行、逐过程执行标识库中代码的能力。
- 能够方便的存取最近使用过的文件或项目。
- 拥有对包、类型、变量、函数和方法的智能代码补全的功能。
- 能够对项目或包中的代码建立抽象语法树视图(AST-view)。
- 内置 Go 的相关工具。
- 能够方便完整地查阅 Go 文档。
- 能够方便地在不同的 Go 环境之间切换。
- 能够导出不同格式的代码文件,如:PDF,HTML 或格式化后的代码。
- 针对一些特定的项目有项目模板,如:Web 应用,App Engine 项目,从而能够更快地开始开发工作。
- 具备代码重构的能力。
- 集成像 hg 或 git 这样的版本控制工具。
- 集成 Google App Engine 开发及调试的功能。
2)编辑器和集成开发环境
这些编辑器包含了代码高亮和其它与 Go 有关的一些使用工具:Emacs、Vim、Xcode 6、KD Kate、TextWrangler、BBEdit、McEdit、TextMate、TextPad、JEdit、SciTE、Nano、Notepad++、Geany、SlickEdit、IntelliJ IDEA 和 Sublime Text 2。
你可以将 Linux 的文本编辑器 GEdit 改造成一个很好的 Go 开发工具,详见页面:http://gohelp.wordpress.com/。
Sublime Text 是一个革命性的跨平台(Linux、Mac OS X、Windows)文本编辑器,它支持编写非常多的编程语言代码。对于 Go 而言,它有一个插件叫做 GoSublime 来支持代码补全和代码模版。
这里还有一些更加高级的 Go 开发工具,其中一些是以插件的形式利用本身是作为开发 Java 的工具。
IntelliJ Idea Plugin 是一个 IntelliJ IDEA 的插件,具有很好的操作体验和代码补全功能。
LiteIDE 这是一款专门针对 Go 开发的集成开发环境,在编辑、编译和运行 Go 程序和项目方面都有非常好的支持。同时还包括了对源代码的抽象语法树视图和一些内置工具(此开发环境由国人 vfc 大叔开发)。
GoClipse 是一款 Eclipse IDE 的插件,拥有非常多的特性以及通过 GoCode 来实现代码补全功能。
如果你对集成开发环境都不是很熟悉,那就使用 LiteIDE 吧,另外使用 GoClipse 或者 IntelliJ Idea Plugin 也是不错的选择。
代码补全:一般都是通过内置 GoCode 实现的(如:LieteIDE、GoClipse),如果需要手动安装 GoCode,在命令行输入指令 go get -u github.com/nsf/gocode 即可(务必事先配置好 Go 环境变量) 。
接下来会对这三个集成开发环境做更加详细的说明。
LiteIDE
这款 IDE 的当前最新版本号为 X27,你可以从 GitHub 页面获取详情。
LiteIDE 是一款非常好用的轻量级 Go 集成开发环境(基于 QT、Kate 和 SciTE),包含了跨平台开发及其它必要的特性,对代码编写、自动补全和运行调试都有极佳的支持。它采用了 Go 项目的概念来对项目文件进行浏览和管理,它还支持在各个 Go 开发环境之间随意切换以及交叉编译的功能。
同时,它具备了抽象语法树视图的功能,可以清楚地纵览项目中的常量、变量、函数、不同类型以及他们的属性和方法。
3)调试器
应用程序的开发过程中调试是必不可少的一个环节,因此有一个好的调试器是非常重要的,可惜的是,Go 在这方面的发展还不是很完善。目前可用的调试器是 gdb,最新版均以内置在集成开发环境 LiteIDE 和 GoClipse 中,但是该调试器的调试方式并不灵活且操作难度较大。
如果你不想使用调试器,你可以按照下面的一些有用的方法来达到基本调试的目的:
-
在合适的位置使用打印语句输出相关变量的值(
print
/println
和fmt.Print
/fmt.Println
/fmt.Printf
)。 -
在
fmt.Printf
中使用下面的说明符来打印有关变量的相关信息:%+v
打印包括字段在内的实例的完整信息%#v
打印包括字段和限定类型名称在内的实例的完整信息%T
打印某个类型的完整说明
-
使用 panic 语句来获取栈跟踪信息(直到 panic 时所有被调用函数的列表)。
-
使用关键字 defer 来跟踪代码执行过程。
4)构建并运行 Go 程序
在大多数 IDE 中,每次构建程序之前都会自动调用源码格式化工具 gofmt 并保存格式化后的源文件。如果构建成功则不会输出任何信息,而当发生编译时错误时,则会指明源码中具体第几行出现了什么错误,如:a declared and not used。一般情况下,你可以双击 IDE 中的错误信息直接跳转到发生错误的那一行。
如果程序执行一切顺利并成功退出后,将会在控制台输出 Program exited with code 0。
从 Go 1 版本开始,使用 Go 自带的更加方便的工具来构建应用程序:
go build
编译并安装自身包和依赖包go install
安装自身包和依赖包
5)格式化代码
Go 开发团队不想要 Go 语言像许多其它语言那样总是在为代码风格而引发无休止的争论,浪费大量宝贵的开发时间,因此他们制作了一个工具:go fmt(gofmt)。这个工具可以将你的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。遵循统一的代码风格是 Go 开发中无可撼动的铁律,因此你必须在编译或提交版本管理系统之前使用 gofmt 来格式化你的代码。
尽管这种做法也存在一些争论,但使用 gofmt 后你不再需要自成一套代码风格而是和所有人使用相同的规则。这不仅增强了代码的可读性,而且在接手外部 Go 项目时,可以更快地了解其代码的含义。此外,大多数开发工具也都内置了这一功能。
Go 对于代码的缩进层级方面使用 tab 还是空格并没有强制规定,一个 tab 可以代表 4 个或 8 个空格。在实际开发中,1 个 tab 应该代表 4 个空格,而在本身的例子当中,每个 tab 代表 8 个空格。至于开发工具方面,一般都是直接使用 tab 而不替换成空格。
在命令行输入 gofmt –w program.go 会格式化该源文件的代码然后将格式化后的代码覆盖原始内容(如果不加参数 -w则只会打印格式化后的结果而不重写文件);gofmt -w *.go 会格式化并重写所有 Go 源文件;gofmt map1 会格式化并重写 map1 目录及其子目录下的所有 Go 源文件。
gofmt 也可以通过在参数 -r 后面加入用双引号括起来的替换规则实现代码的简单重构,规则的格式: -> 。
实例:
gofmt -r “(a) -> a” –w *.go
上面的代码会将源文件中没有意义的括号去掉。
gofmt -r “a[n:len(a)] -> a[n:]” –w *.go
上面的代码会将源文件中多余的 len(a)
去掉。
gofmt –r ‘A.Func1(a,b) -> A.Func2(b,a)’ –w *.go
上面的代码会将源文件中符合条件的函数的参数调换位置。
如果想要了解有关 gofmt
的更多信息,请访问该页面:http://golang.org/cmd/gofmt/
6)生成代码文档
go doc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。
它也可以作为一个提供在线文档浏览的 web 服务器,http://golang.org 就是通过这种形式实现的。
一般用法:
go doc package
获取包的文档注释,例如:go doc fmt
会显示使用godoc
生成的fmt
包的文档注释。go doc package/subpackage
获取子包的文档注释,例如:go doc container/list
。go doc package function
获取某个函数在某个包中的文档注释,例如:go doc fmt Printf
会显示有关fmt.Printf()
的使用说明。
这个工具只能获取在 Go 安装目录下 .../go/src 中的注释内容。此外,它还可以作为一个本地文档浏览 web 服务器。在命令行输入 godoc -http=:6060,然后使用浏览器打开http://localhost:6060 后,你就可以看到本地文档浏览服务器提供的页面。
godoc 也可以用于生成非标准库的 Go 源码文件的文档注释。
如果想要获取更多有关 godoc
的信息,请访问该页面:http://golang.org/cmd/godoc/(在线版的第三方包 godoc
可以使用 Go Walker)。
7)其它工具
Go 自带的工具集主要使用脚本和 Go 语言自身编写的,目前版本的 Go 实现了以下三个工具:
go install
是安装 Go 包的工具,类似 Ruby 中的 rubygems。主要用于安装非标准库的包文件,将源代码编译成对象文件。go fix
用于将你的 Go 代码从旧的发行版迁移到最新的发行版,它主要负责简单的、重复的、枯燥无味的修改工作,如果像 API 等复杂的函数修改,工具则会给出文件名和代码行数的提示以便让开发人员快速定位并升级代码。Go 开发团队一般也使用这个工具升级 Go 内置工具以及 谷歌内部项目的代码。go fix
之所以能够正常工作是因为 Go 在标准库就提供生成抽象语法树和通过抽象语法树对代码进行还原的功能。该工具会尝试更新当前目录下的所有 Go 源文件,并在完成代码更新后在控制台输出相关的文件名称。go test
是一个轻量级的单元测试框架。
Go 的源文件以 .go 为后缀名存储在计算机中,这些文件名均由小写字母组成,如 scanner.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 scanner_test.go 。文件名不包含空格或其他特殊字符。
一个源文件可以包含任意多行的代码,Go 本身没有对源文件的大小进行限制。
你会发现在 Go 代码中的几乎所有东西都有一个名称或标识符。另外,Go 语言也是区分大小写的,这与 C 家族中的其它语言相同。有效的标识符必须以字符(可以使用任何 UTF-8 编码的字符或 _)开头,然后紧跟着 0 个或多个字符或 Unicode 数字,如:X56、group1、_x23、i、өԑ12。
以下是无效的标识符:
- 1ab(以数字开头)
- case(Go 语言的关键字)
- a+b(运算符是不允许的)
_
本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个这个标识符作为变量对其它变量的进行赋值或运算。
在编码过程中,你可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。
下面列举了 Go 代码中会使用到的 25 个关键字或保留字:
breakdefaultfuncinterfacecasedefergomapchanelsegotopackageconstfallthroughifrangecontinueforimportreturn之所以刻意地将 Go 代码中的关键字保持的这么少,是为了简化在编译过程第一步中的代码解析。和其它语言一样,关键字不能够作标识符使用。
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数,它们的作用都将在接下来的章节中进行进一步地讲解。
appendboolbytecapclosecomplexcopyfalsefloat32float64imagintint32int64iotalenmakenewprintprintlnrealrecoverstringtrue程序一般由关键字、常量、变量、运算符、类型和函数组成。
程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。
程序中可能会使用到这些标点符号:.、,、;、: 和 …。
程序的代码通过语句来实现结构化。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。
2、Go 程序的基本结构和要素示例: hello_world.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
1)包的概念、导入与可见性
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。另外要注意的是,所有的包名都应该使用小写字母。
标准库:
在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Windows 下,标准库的位置在 Go 根目录下的子目录pkg\windows_386 中;在 Linux 下,标准库在 Go 根目录下的子目录 pkg\linux_amd64 中(如果是安装的是 32 位,则在linux_386 目录中)。一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。
Go 的标准库包含了大量的包(如:fmt 和 os),但是你也可以创建自己的包。
如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。
属于同一个包的源文件必须全部被一起编译,一个包既是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。
如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。
如果 A.go 依赖 B.go,而 B.go 又依赖 C.go:
- 编译
C.go
,B.go
, 然后是A.go
. - 为了编译
A.go
, 编译器读取的是B.o
而不是C.o
.
这种机制对于编译大型的项目时可以显著地提升编译速度。
每一段代码只会被编译一次。
一个 Go 程序是通过 import 关键字将一组包链接在一起。
import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。
如果需要多个包,它们可以被分别导入:
import "fmt"
import "os"
或:
import "fmt"; import "os"
但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 const、var 和 type 的声明或定义):
import (
"fmt"
"os"
)
它甚至还可以更短的形式,但使用 gofmt 后将会被强制换行:
import ("fmt"; "os")
当你导入多个包时,导入的顺序会按照字母排序。
如果包名不是以 . 或 / 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。
导入包即等同于包含了这个包的所有的代码对象。
除了符号 _,包中所有代码对象的标识符必须是唯一的,以避免名称冲突。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件:
可见性规则
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:pack1.Thing(pack1 在这里是不可以省略的)。
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如pack1.Thing 和 pack2.Thing。
你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:import fm "fmt"。下面的代码展示了如何使用包的别名:
示例: alias.go
package main
import fm "fmt" // alias3
func main() {
fm.Println("hello, world")
}
注意事项:
如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 imported and not used: os
,这正是遵循了 Go 的格言:“没有不必要的代码!“。
包的分级声明和初始化:
你可以在使用 import 导入包之后定义或声明 0 个或多个常量(const)、变量(var)和类型(type),这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用(如 gotemplate.go 源文件中的 c 和 v),然后声明一个或多个函数(func)。
2)函数
这是定义一个函数最简单的格式:
func functionName()
你可以在括号()中写入 0 个或多个函数的参数(使用逗号 ,
分隔),每个参数的名称后面必须紧跟着该参数的类型。
main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 undefined: main.main。main 函数即没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main 函数添加了参数或者返回类型,将会引发构建错误:
func main must have no arguments and no return values results.
在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 main.main()(如:C 语言),该函数一旦返回就表示程序已成功执行并立即退出。
函数里的代码(函数体)使用大括号 {} 括起来。
左大括号 { 必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示:
`build-error: syntax error: unexpected semicolon or newline before {`
这是因为编译器会产生 func main() ; 这样的结果,很明显这错误的。
Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发像上面这样的错误。
右大括号 } 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:
func Sum(a, b int) int { return a + b }
对于大括号 {} 的使用规则在任何时候都是相同的(如:if 语句等)。
因此符合规范的函数一般写成如下的形式:
func functionName(parameter_list) (return_value_list) {
…
}
其中:
- parameter_list 的形式为 (param1 type1, param2 type2, …)
- return_value_list 的形式为 (ret1 type1, ret2 type2, …)
只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。
下面这一行调用了 fmt 包中的 Println 函数,可以将字符串输出到控制台,并在最后自动增加换行字符 \n:
fmt.Println("hello, world")
使用 fmt.Print("hello, world\n") 可以得到相同的结果。
Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。
单纯地打印一个字符串或变量甚至可以使用预定义的方法来实现,如:print、println:print("ABC")、println("ABC")、println(i)(带一个变量 i)。
这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 fmt 中的相关函数。
当被调用函数的代码执行到结束符 } 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。
程序正常退出的代码为 0 即 Program exited with code 0;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。
3)注释
示例: hello_world2.go
package main
import "fmt" // Package implementing formatted I/O.
func main() {
fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n")
}
上面这个例子通过打印 Καλημέρα κόσμε; or こんにちは 世界,展示了如何在 Go 中使用国际化字符,以及如何使用注释。
注释不会被编译,但可以通过 godoc 来使用。
单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。当开发人员需要了解包的一些情况时,自然会用 godoc 来显示包的文档说明,在首行的简要注释之后可以用成段的注释来进行更详细的说明,而不必拥挤在一起。另外,在多段注释之间应以空行分隔加以区分。
示例:
// Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman
几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..." 作为开头。
示例:
// enterOrbit causes Superman to fly into low Earth orbit, a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
...
}
godoc 工具会收集这些注释并产生一个技术文档。
4)类型
可以包含数据的变量(或常量)可以使用不同的数据类型或类型来保存数据。使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。
类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。
函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如:
func FunctionName (a typea, b typeb) typeFunc
你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var:
return var
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 ()
将它们括起来,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
示例: 函数 Atoi:func Atoi(s string) (i int, err error)
返回的形式:
return var1, var2
这种多返回值一般用于判断某个函数是否执行成功(true/false)或与其它返回值一同返回错误消息(详见之后的并行赋值)。
使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以定义一个已经存在的类型的别名,如:
type IZ int
这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。
然后我们可以使用下面的方式声明变量:
var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能。
如果你有多个类型需要定义,可以使用因式分解关键字的方式,例如:
type (
IZ int
FZ float64
STR string
)
每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。
5)Go 程序的一般结构
下面的程序可以被顺利编译但什么都做不了,不过这很好地展示了一个 Go 程序的首选结构。这种结构并没有被强制要求,编译器也不关心 main 函数在前还是变量的声明在前,但使用统一的结构能够在从上至下阅读 Go 代码时有更好的体验。
所有的结构将在这一章或接下来的章节中进一步地解释说明,但总体思路如下:
- 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
- 如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
- 如果当前包是 main 包,则定义 main 函数。
- 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。
示例: gotemplate.go
package main
import (
"fmt"
)
const c = "C"
var v int = 5
type T struct{}
func init() { // initialization of package
}
func main() {
var a int
Func1()
// ...
fmt.Println(a)
}
func (t T) Method1() {
//...
}
func Func1() { // exported function Func1
//...
}
Go 程序的执行(程序启动)顺序如下:
- 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
- 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
- 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init 函数的话,则调用该函数。
- 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。
6)类型转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数):
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)。
示例:
a := 5.0
b := int(a)
但这只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。
具有相同底层类型的变量之间可以相互转换:
var a IZ = 5
c := int(a)
d := IZ(c)
7)Go 命名规范
干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get... 之类的字符,如果是用于修改某个对象,则使用 SetName。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称。
3、常量常量使用关键字 const 定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:const identifier [type] = value,例如:
const Pi = 3.14159
在 Go 语言中,你可以省略类型说明符 [type]
,因为编译器可以根据变量的值来推断其类型。
-
显式类型定义:
const b string = "abc"
-
隐式类型定义:
const b = "abc"
一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。
var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int
常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
-
正确的做法:
const c1 = 2/3
-
错误的做法:
const c2 = getNumber()
// 引发构建错误:getNumber() used as value
因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。
数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:
const Ln2= 0.693147180559945309417232121458\
176568075500134360255254120680009
const Log2E= 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 > 97
根据上面的例子我们可以看到,反斜杠 \
可以在常量表达式中作为多行的连接符使用。
与各种类型的数字型变量相比,你无需担心常量之间的类型转换问题,因为它们都是非常理想的数字。
不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。另外,常量也允许使用并行赋值的形式:
const beef, two, c = “meat”, 2, “veg”
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
Monday, Tuesday, Wednesday = 1, 2, 3
Thursday, Friday, Saturday = 4, 5, 6
)
常量还可以用作枚举:
const (
Unknown = 0
Female = 1
Male = 2
)
现在,数字 0、1 和 2 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case 结构。
在这个例子中,iota 可以被用作枚举值:
const (
a = iota
b = iota
c = iota
)
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (
a = iota
b
c
)
iota 也可以用在表达式中,如:iota + 50。在每遇到一个新的常量块或单个常量声明时, iota 都会重置为 0( 简单地讲,每遇到一次 const 关键字,iota 就重置为 0 )。
当然,常量之所以为常量就是恒定不变的量,因此我们无法在程序运行过程中修改它的值;如果你在代码中试图修改常量的值则会引发编译错误。
引用 time 包中的一段代码作为示例:一周中每天的名称。
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
你也可以使用某个类型作为枚举常量的类型:
type Color int
const (
RED Color = iota // 0
ORANGE // 1
YELLOW // 2
GREEN // ..
BLUE
INDIGO
VIOLET // 6
)
4、变量
声明变量的一般形式是使用 var 关键字:var identifier type。
需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢?
首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如:int* a, b;。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写(你可以在 Go 语言的声明语法 页面找到有关于这个话题的更多讨论)。
而在 Go 中,则可以和轻松地将它们都声明为指针类型:
var a, b *int
其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。
示例:
var a int
var b bool
var str string
你也可以改写成这种形式:
var (
a int
b bool
str string
)
这种因式分解关键字的写法一般用于声明全局变量。
当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate。
但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写。
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。之后我们将会学习到像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。
尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。
变量可以编译期间就被赋值,赋值给变量使用运算符等号 =,当然你也可以在运行时对变量进行赋值操作。
示例:
a = 15
b = false
一般情况下,只有类型相同的变量之间才可以相互赋值,例如:
a = b
声明与赋值(初始化)语句也可以组合起来。
示例:
var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"
但是 Go 编译器的智商已经高到可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 是在编译时就已经完成推断过程。因此,你还可以使用下面的这些形式来声明及初始化变量:
var a = 15
var b = false
var str = "Go says hello to the world!"
或:
var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)
不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,例如:
var n int64 = 2
然而,var a 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据。变量的类型也可以在运行时实现自动推断,例如:
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 :=
,例如:
a := 1
下面这个例子展示了如何在运行时获取所在的操作系统类型,它通过 os 包中的函数 os.Getenv() 来获取环境变量中的值,并保存到 string 类型的局部变量 path 中。
示例: goos.go
package main
import (
"fmt"
"os"
)
func main() {
var goos string = os.Getenv("GOOS")
fmt.Printf("The operating system is: %s\n", goos)
path := os.Getenv("PATH")
fmt.Printf("Path is %s\n", path)
}
如果你在 Windows 下运行这段代码,则会输出 The operating system is: windows 以及相应的环境变量的值;如果你在 Linux 下运行这段代码,则会输出 The operating system is: linux 以及相应的的环境变量的值。
这里用到了 Printf 的格式化输出的功能。
1)值类型和引用类型
程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:
另外,像数组和结构这些复合类型也是值类型。
当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝:
你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址为称之为指针,这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。
如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices,maps和 channel。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
2)打印
函数 Printf 可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:
func Printf(format string, list of variables to be printed)
在示例中,格式化字符串为:"The operating system is: %s\n"。
这个格式化字符串可以含有一个或多个的格式化标识符,例如:%..,其中 .. 可以被不同类型所对应的标识符替换,如%s 代表字符串标识符、%v 代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。
函数 fmt.Sprintf 与 Printf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者,因此你可以在程序中使用包含变量的字符串,具体例子可以参见示例simple_tcp_server.go。
函数 fmt.Print 和 fmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:
fmt.Print("Hello:", 23)
将输出:Hello: 23。
3)简短形式,使用 := 赋值操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再在 Example 的最后一个声明语句写上 var 关键字就显得有些多余了,因此我们可以将它们简写为 a := 50 或 b := false。
a 和 b 的类型(int 和 bool)将由编译器自动推断。
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。
注意事项:
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:
func main() {
var a string = "abc"
fmt.Println("hello, world")
}
尝试编译这段代码将得到错误 a declared and not used。
此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用 fmt.Println("hello, world", a) 会移除错误。
但是全局变量是允许声明但不使用。
其他的简短形式为:
同一类型的多个变量可以声明在同一行,如:
var a, b, c int
这是将类型写在标识符后面的一个重要原因。
多变量可以在同一行进行赋值,如:
a, b, c = 5, 7, "abc"
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:
a, b, c := 5, 7, "abc"
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 "abc"。
这被称为 并行 或 同时 赋值。
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a。
(在 Go 语言中,这样省去了使用交换函数的必要)
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)。
4)init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
每一个源文件都可以包含且只包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。
示例:init.go:
package trans
import "math"
var Pi float64
func init() {
Pi = 4 * math.Atan(1) // init() function computes Pi
}
在它的 init 函数中计算变量 Pi 的初始值。
示例: user_init.go 中导入了包 trans(在相同的路径中)并且使用到了变量 Pi:
package main
import (
"fmt"
"./trans"
)
var twoPi = 2 * trans.Pi
func main() {
fmt.Printf("2*Pi = %g\n", twoPi) // 2*Pi = 6.283185307179586
}
init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend():
func init() {
// setup preparations
go backend()
}
推断以下程序的输出,并解释你的答案,然后编译并执行它们。
练习1: local_scope.go:
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() { print(a) }
func m() {
a := "O"
print(a)
}
练习2: global_scope.go:
package main
var a = "G"
func main() {
n()
m()
n()
}
func n() {
print(a)
}
func m() {
a = "O"
print(a)
}
练习3: function_calls_function.go
package main
var a string
func main() {
a = "G"
print(a)
f1()
}
func f1() {
a := "O"
print(a)
f2()
}
func f2() {
print(a)
}
5、基本类型和运算符
我们将在这个部分讲解有关布尔型、数字型和字符型的相关知识。
表达式是一种特定的类型的值,它可以由其它的值以及运算符组合而成。每个类型都定义了可以和自己结合的运算符集合,如果你使用了不在这个集合中的运算符,则会在编译时获得编译错误。
一元运算符只可以用于一个值的操作(作为后缀),而二元运算符则可以和两个值或者操作数结合(作为中缀)。
只有两个类型相同的值才可以和二元运算符结合,另外要注意的是,Go 是强类型语言,因此不会进行隐式转换,任何不同类型之间的转换都必须显式说明。Go 不存在像 C 和 Java 那样的运算符重载,表达式的解析顺序是从左至右。
优先级越高的运算符在条件相同的情况下将被优先执行。但是你可以通过使用括号将其中的表达式括起来,以人为地提升某个表达式的运算优先级。
1)布尔类型 bool
一个简单的例子:var b bool = true。
布尔型的值只可以是常量 true 或者 false。
两个类型相同的值可以使用相等 == 或者不等 != 运算符来进行比较并获得一个布尔型的值。
当相等运算符两边的值是完全相同的值的时候会返回 true,否则返回 false,并且只有在两个的值的类型相同的情况下才可以使用。
示例:
var aVar = 10
aVar == 5 -> false
aVar == 10 -> true
当不等运算符两边的值是不同的时候会返回 true,否则返回 false。
示例:
var aVar = 10
aVar != 5 -> true
aVar != 10 -> false
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。
布尔型的常量和变量也可以通过和逻辑运算符(非 !、和 &&、或 ||)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
逻辑值可以被用于条件结构中的条件语句,以便测试某个条件是否满足。另外,和 &&、或 || 与相等 == 或不等 != 属于二元运算符,而非 ! 属于一元运算符。在接下来的内容中,我们会使用 T 来代表条件符合的语句,用 F 来代表条件不符合的语句。
Go 语言中包含以下逻辑运算符:
非运算符:!
!T -> false
!F -> true
非运算符用于取得和布尔值相反的结果。
和运算符:&&
T && T -> true
T && F -> false
F && T -> false
F && F -> false
只有当两边的值都为 true 的时候,和运算符的结果才是 true。
或运算符:||
T || T -> true
T || F -> true
F || T -> true
F || F -> false
只有当两边的值都为 false 的时候,或运算符的结果才是 false,其中任意一边的值为 true 就能够使得该表达式的结果为 true。
在 Go 语言中,&& 和 || 是具有快捷性质的运算符,当运算符左边表达式的值已经能够决定整个表达式的值的时候(&& 左边的值为 false,|| 左边的值为 true),运算符右边的表达式将不会被执行。利用这个性质,如果你有多个条件判断,应当将计算过程较为复杂的表达式放在运算符的右侧以减少不必要的运算。
利用括号同样可以升级某个表达式的运算优先级。
在格式化输出时,你可以使用 %t 来表示你要输出的值为布尔型。
布尔值(以及任何结果为布尔值的表达式)最常用在条件结构的条件语句中,例如:if、for 和 switch 结构。
对于布尔值的好的命名能够很好地提升代码的可读性,例如以 is 或者 Is 开头的isSorted、isFinished、isVisivle,使用这样的命名能够在阅读代码的获得阅读正常语句一样的良好体验,例如标准库中的 unicode.IsDigit(ch)。
2)数字类型
(1)整型 int 和浮点型 float
Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码(详情参见 二的补码 页面)。
Go 也有基于架构的类型,例如:int、uint 和 uintptr。
这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
int
和uint
在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。uintptr
的长度被设定为足够存放一个指针即可。
Go 语言中没有 float 类型。
与操作系统架构无关的类型都有固定的大小,并在类型的名称中就可以看出来:
整数:
- int8(-128 -> 127)
- int16(-32768 -> 32767)
- int32(-2,147,483,648 -> 2,147,483,647)
- int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
无符号整数:
- uint8(0 -> 255)
- uint16(0 -> 65,535)
- uint32(0 -> 4,294,967,295)
- uint64(0 -> 18,446,744,073,709,551,615)
浮点型(IEEE-754 标准):
- float32(+- 1e-45 -> +- 3.4 * 1e38)
- float64(+- 5 * 1e-324 -> 107 * 1e308)
int 型是计算最快的一种类型。
整型的零值为 0,浮点型的零值为 0.0。
float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,你在使用 == 或者 != 来比较浮点数时应当非常小心。你最好在正式使用前测试对于精确度要求较高的运算。
你应该尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。
你可以通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。
你可以使用 a := uint64(0) 来同时完成类型转换和赋值操作,这样 a 的类型就是 uint64。
Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用,下面这个程序很好地解释了这个现象(该程序无法通过编译):
示例: type_mixing.go
package main
func main() {
var a int
var b int32
a = 15
b = a + a // 编译错误
b = b + 5 // 因为 5 是常量,所以可以通过编译
}
如果你尝试编译该程序,则将得到编译错误 cannot use a + a (type int) as type int32 in assignment。
同样地,int16也不能够被隐式转换为 int32。
下面这个程序展示了通过显示转换来避免这个问题。
示例: casting.go
package main
import "fmt"
func main() {
var n int16 = 34
var m int32
// compiler error: cannot use n (type int16) as type int32 in assignment
//m = n
m = int32(n)
fmt.Printf("32 bit int is: %d\n", m)
fmt.Printf("16 bit int is: %d\n", n)
}
输出:
32 bit int is: 34
16 bit int is: 34
格式化说明符:
在格式化字符串里,%d 用于格式化整数(%x 和 %X 用于格式化 16 进制表示的数字),%g 用于格式化浮点型(%f 输出浮点数,%e 输出科学计数表示法),%0d 用于规定输出定长的整数,其中开头的数字 0 是必须的。
%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串%5.2e 来输出 3.4 的结果为 3.40e+00。
数字值转换:
当进行类似 a32bitInt = int32(a32Float) 的转换时,小数点后的数字将被丢弃。这种情况一般发生当从取值范围较大的类型转换为取值范围较小的类型时,或者你可以写一个专门用于处理类型转换的函数来确保没有发生精度的丢失。下面这个例子展示如何安全地从 int 型转换为 int8:
func Uint8FromInt(n int) (uint8, error) {
if 0 0
0 & 0 -> 0
按位或 |
:
对应位置上的值经过或运算结果,并将 T(true)替换为 1,将 F(false)替换为 0
1 | 1 -> 1
1 | 0 -> 1
0 | 1 -> 1
0 | 0 -> 0
按位异或 ^
:
对应位置上的值根据以下规则组合:
1 ^ 1 -> 0
1 ^ 0 -> 1
0 ^ 1 -> 1
0 ^ 0 -> 0
位清除 &^
:将指定位置上的值设置为 0。
一元运算符:
-
按位补足
^
:该运算符与异或运算符一同使用,即
m^x
,对于无符号 x 使用“全部位设置为 1”,对于有符号 x 时使用m=-1
。例如:^2 = ^10 = -01 ^ 10 = -11
-
位左移
20111221 }
输出的结果已经写在每行 // 的后面。
如果你需要在应用程序在经过一定时间或周期执行某项任务(事件处理的特例),则可以使用 time.After 或者time.Ticker:后面我们讨论这些有趣的事情。 另外,time.Sleep(Duration d) 可以实现对某个进程(实质上是 goroutine)时长为 d 的暂停。
9、指针不像 Java 和 .NET,Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。通过给予程序员基本内存布局,Go 语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的:指针对于性能的影响是不言而喻的,而如果你想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。
由于各种原因,指针对于使用面向对象编程的现代程序员来说可能显得有些陌生,不过我们将会在这一小节对此进行解释,并在未来的章节中展开深入讨论。
程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b0820 或0xf84001d7f0。
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
下面的代码片段(下面示例 pointer.go)可能输出 An integer: 5, its location in memory: 0x6b0820(这个值随着你每次运行程序而变化)。
var i1 = 5 fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)
这个地址可以存储在一个叫做指针的特殊数据类型中,在本例中这是一个指向 int 的指针,即 i1:此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它:
var intP *int
然后使用 intP = &i1 是合法的,此时 intP 指向 i1(指针的格式化标识符为 %p)。
intP 存储了 i1 的内存地址;它指向了 i1 的位置,它引用了变量 i1。
一个指针变量可以指向任何一个值的内存地址,它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容,这里的 * 号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
一个指针变量通常缩写为 ptr。
注意事项:
在书写表达式类似 var p *type 时,切记在 * 号和指针名称间留有一个空格,因为 - var p*type 是语法正确的,但是在更复杂的表达式中,它容易被误认为是一个乘法表达式!
符号 * 可以放在一个指针前,如 *intP,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。
对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)。
现在,我们应当能理解 pointer.go 中的整个程序和他的输出:
示例: pointer.go:
package main import "fmt" func main() { var i1 = 5 fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1) var intP *int intP = &i1 fmt.Printf("The value at memory location %p is %d\n", intP, *intP) }
输出:
An integer: 5, its location in memory: 0x24f0820 The value at memory location 0x24f0820 is 5
程序 string_pointer.go 为我们展示了指针对string的例子。
它展示了分配一个新的值给 *p 并且更改这个变量自己的值(这里是一个字符串)。
示例: string_pointer.go
package main import "fmt" func main() { s := "good bye" var p *string = &s *p = "ciao" fmt.Printf("Here is the pointer p: %p\n", p) // prints address fmt.Printf("Here is the string *p: %s\n", *p) // prints string fmt.Printf("Here is the string s: %s\n", s) // prints same string }
输出:
Here is the pointer p: 0x2540820 Here is the string *p: ciao Here is the string s: ciao
通过对 *p 赋另一个值来更改“对象”,这样 s 也会随之更改。
注意事项:
你不能得到一个文字或常量的地址,例如:
const i = 5 ptr := &i //error: cannot take the address of i ptr2 := &10 //error: cannot take the address of 10
所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 语言的代码中是不合法的。
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
另一方面(虽然不太可能),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。
指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。
如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。
对一个空指针的反向引用是不合法的,并且会使程序崩溃:
示例: testcrash.go:
四、控制结构 1、if-else 结构package main func main() { var p *int = nil *p = 0 } // in Windows: stops only with: // runtime error: invalid memory address or nil pointer dereference
if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。
if condition { // do something }
如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if 和 else 后的两个代码块是相互独立的分支,只可能执行其中一个。
if condition { // do something } else { // do something }
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
if condition1 { // do something } else if condition2 { // do something else }else { // catch-all or default }
else-if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else-if 结构。如果你必须使用这种形式,则把尽可能先满足的条件放在前面。
即使当代码块之间只有一条语句时,大括号也不可被省略(尽管有些人并不赞成,但这还是符合了软件工程原则的主流做法)。
关键字 if 和 else 之后的左大括号
{
必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号}
必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。非法的 Go 代码:
if x{ } else { // 无效的 }
要注意的是,在你使用 gofmt 格式化代码之后,每个分支内的代码都会缩进 4 个或 8 个空格,或者是 1 个 tab,并且右大括号与对应的 if 关键字垂直对齐。
在有些情况下,条件语句两侧的括号是可以被省略的;当条件比较复杂时,则可以使用括号让代码更易读。条件允许是符合条件,需使用 &&、|| 或 !,你可以使用括号来提升某个表达式的运算优先级,并提高代码的可读性。
一种可能用到条件语句的场景是测试变量的值,在不同的情况执行不同的语句,不过后面讲到的 switch 结构会更适合这种情况。
示例: booleans.go
package main import "fmt" func main() { bool1 := true if bool1 { fmt.Printf("The value is true\n") } else { fmt.Printf("The value is false\n") } }
输出:
The value is true
注意事项:这里不需要使用 if bool1 == true 来判断,因为 bool1 本身已经是一个布尔类型的值。
这种做法一般都用在测试 true 或者有利条件时,但你也可以使用取反 ! 来判断值的相反结果,如:if !bool1 或者if !(condition)。后者的括号大多数情况下是必须的,如这种情况:if !(var1 == var2)。
当 if 结构内有 break、continue、goto 或者 return 语句时,Go 代码的常见写法是省略 else 部分。无论满足哪个条件都会返回 x 或者 y 时,一般使用以下写法:
if condition { return x } return y
注意事项:不要同时在 if-else 结构的两个分支里都使用 return 语句,这将导致编译报错 function ends without a return statement(你可以认为这是一个编译器的 Bug 或者特性)。(该问题已经在 Go 1.1 中被修复或者说改进 )
这里举一些有用的例子:
判断一个字符串是否为空:
if str == "" { ... }
if len(str) == 0 {...}
判断运行 Go 程序的操作系统类型,这可以通过常量 runtime.GOOS 来判断:
if runtime.GOOS == "windows" { . .. } else { // Unix-like . .. }
这段代码一般被放在 init() 函数中执行。这儿还有一段示例来演示如何根据操作系统来决定输入结束的提示:
var prompt = "Enter a digit, e.g. 3 "+ "or %s to quit." func init() { if runtime.GOOS == "windows" { prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter") } else { //Unix-like prompt = fmt.Sprintf(prompt, "Ctrl+D") } }
函数 Abs() 用于返回一个整型数字的绝对值:
func Abs(x int) int { if x < 0 { return -x } return x }
isGreater 用于比较两个整型数字的大小:
func isGreater(x, y int) bool { if x > y { return true } return false }
在第四种情况中,if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):
if initialization; condition { // do something }
例如:
val := 10 if val > max { // do something }
你也可以这样写:
if val := 10; val > max { // do something }
但要注意的是,使用简短方式 := 声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。如果变量在 if 结构之前就已经存在,那么在 if 结构中,该变量原来的值会被隐藏。最简单的解决方案就是不要在初始化语句中声明变量。
示例: ifelse.go
package main import "fmt" func main() { var first int = 10 var cond int if first 0 && first < 5 { fmt.Printf("first is between 0 and 5\n") } else { fmt.Printf("first is 5 or greater\n") } if cond = 5; cond > 10 { fmt.Printf("cond is greater than 10\n") } else { fmt.Printf("cond is not greater than 10\n") } }
输出:
first is 5 or greater cond is not greater than 10
下面的代码片段展示了如何通过在初始化语句中获取函数 process() 的返回值,并在条件语句中作为判定条件来决定是否执行 if 结构中的代码:
2、测试多返回值函数的错误if value := process(data); value > max { ... if value := process(data); value > max { ... }
Go 语言的函数经常使用两个返回值来表示执行是否成功:返回某个值以及 true 表示成功;返回零值(或 nil)和 false 表示失败。当不使用 true 或 false 的时候,也可以使用一个 error 类型的变量来代替作为第二个返回值:成功执行的话,error 的值为 nil,否则就会包含相应的错误信息(Go 语言中的错误类型为 error: var err error)。
这样一来,就很明显需要用一个 if 语句来测试执行结果;由于其符号的原因,这样的形式又称之为 comma,ok 模式(pattern)。
在程序 string_conversion.go 中,函数 strconv.Atoi 的作用是将一个字符串转换为一个整数。之前我们忽略了相关的错误检查:
anInt, _ = strconv.Atoi(origStr)
如果 origStr 不能被转换为整数,anInt 的值会变成 0 而
_
无视了错误,程序会继续运行。这样做是非常不好的:程序应该在最接近的位置检查所有相关的错误,至少需要暗示用户有错误发生并对函数进行返回,甚至中断程序。
我们在第二个版本中对代码进行了改进:
示例 1:
string_conversion2.go
package main import ( "fmt" "strconv" ) func main() { var orig string = "ABC" // var an int var newS string // var err error fmt.Printf("The size of ints is: %d\n", strconv.IntSize) // anInt, err = strconv.Atoi(origStr) an, err := strconv.Atoi(orig) if err != nil { fmt.Printf("orig %s is not an integer - exiting with error\n", orig) return } fmt.Printf("The integer is %d\n", an) an = an + 5 newS = strconv.Itoa(an) fmt.Printf("The new string is: %s\n", newS) }
这是测试 err 变量是否包含一个真正的错误(if err != nil)的习惯用法。如果确实存在错误,则会打印相应的错误信息然后通过 return 提前结束函数的执行。我们还可以使用携带返回值的 return 形式,例如 return err。这样一来,函数的调用者就可以检查函数执行过程中是否存在错误了。
习惯用法:
value, err := pack1.Function1(param1) if err!=nil { fmt.Printf("An error occured in pack1.Function1 with parameter %v", param1) return err } // 未发生错误,继续执行:
由于本例的函数调用者属于 main 函数,所以程序会直接停止运行。
如果我们想要在错误发生的同时终止程序的运行,我们可以使用 os 包的 Exit 函数:
习惯用法:
if err !=nil { fmt.Printf("Program stopping with error %v", err) os.Exit(1) }
此处的退出代码 1 可以使用外部脚本获取到。
有时候,你会发现这种习惯用法被连续重复地使用在某段代码中。
当没有错误发生时,代码继续运行就是唯一要做的事情,所以 if 语句块后面不需要使用 else 分支。
示例 2:我们尝试通过 os.Open 方法打开一个名为 name 的只读文件:
f, err := os.Open(name) if err !=nil { return err } doSomething(f) // 当没有错误发生时,文件对象被传入到某个函数中 doSomething
示例 3:可以将错误的获取放置在 if 语句的初始化部分:
if err := file.Chmod(0664); err !=nil { fmt.Println(err) return err }
示例 4:或者将 ok-pattern 的获取放置在 if 语句的初始化部分,然后进行判断:
if value, ok := readData(); ok { … }
注意事项:
如果您像下面一样,没有为多返回值的函数准备足够的变量来存放结果:
func mySqrt(f float64) (v float64, ok bool) { if f < 0 { return } // error case return math.Sqrt(f),true } func main() { t := mySqrt(25.0) fmt.Println(t) }
您会得到一个编译错误:multiple-value mySqrt() in single-value context。
正确的做法是:
t, ok := mySqrt(25.0) if ok { fmt.Println(t) }
注意事项 2:
当您将字符串转换为整数时,且确定转换一定能够成功时,可以将 Atoi 函数进行一层忽略错误的封装:
func atoi (s string) (n int) { n, _ = strconv.Atoi(s) return }
实际上,fmt 包最简单的打印函数也有 2 个返回值:
count, err := fmt.Println(x) // number of bytes printed, nil or 0, error
当打印到控制台时,可以将该函数返回的错误忽略;但当输出到文件流、网络流等具有不确定因素的输出对象时,应该始终检查是否有错误发生)。
3、switch 结构相比较 C 和 Java 等其它语言而言,Go 语言中的 switch 结构使用上更加灵活。它接受任意形式的表达式:
switch var1 { case val1: ... case val2: ... default: ... }
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。
每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。
一旦成功地匹配到每个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您不需要特别使用 break 语句来表示结束。
因此,程序也不会自动地去执行下一个分支的代码。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。
因此:
switch i { case 0: // 空分支,只有当 i == 0 时才会进入分支 case 1: f() // 当 i == 0 时函数不会被调用 }
并且:
switch i { case 0: fallthrough case 1: f() // 当 i == 0 时函数也会被调用 }
在case ...: 语句之后,您不需要使用花括号将多行语句括起来,但您可以在分支中进行任意形式的编码。当代码块只有一行时,可以直接放置在 case 语句之后。
您同样可以使用 return 语句来提前结束代码块的执行。当您在 switch 语句块中使用 return 语句,并且您的函数是有返回值的,您还需要在 switch 之后添加相应的 return 语句以确保函数始终会返回。
可选的 default 分支可以出现在任何顺序,但最好将它放在最后。它的作用类似与 if-else 语句中的 else,表示不符合任何已给出条件时,执行相关语句。
示例: switch1.go:
package main import "fmt" func main() { var num1 int = 100 switch num1 { case 98, 99: fmt.Println("It's equal to 98") case 100: fmt.Println("It's equal to 100") default: fmt.Println("It's not equal to 98 or 100") } }
输出:
It's equal to 100
后面我们会使用 switch 语句判断从键盘输入的字符。switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时,该分支的代码会被执行。这看起来非常像链式的 if-else 语句,但是在测试条件非常多的情况下,提供了可读性更好的书写方式。
switch { case condition1: ... case condition2: ... default: ... }
例如:
switch { case i < 0: f1() case i == 0: f2() case i > 0: f3() }
任何支持进行相等判断的类型都可以作为测试表达式的条件,包括 int、string、指针等。
示例: switch2.go:
package main import "fmt" func main() { var num1 int = 7 switch { case num1 < 0: fmt.Println("Number is negative") case num1 > 0 && num1 < 10: fmt.Println("Number is between 0 and 10") default: fmt.Println("Number is 10 or greater") } }
输出:
Number is between 0 and 10
switch 语句的第三种形式是包含一个初始化语句:
switch initialization { case val1: ... case val2: ... default: ... }
这种形式可以非常优雅地进行条件判断:
switch result := calculate(); { case result < 0: ... case result > 0: ... default: // 0 }
在下面这个代码片段中,变量 a 和 b 被平行初始化,然后作为判断条件:
switch a, b := x[i], y[j]; { case a < b: t = -1 case a == b: t = 0 case a > b: t = 1 }
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
问题:
请说出下面代码片段输出的结果:
k := 6 switch k { case 4: fmt.Println("was {if .}}{{.}}{end}} package urlshort func main() -> func init()
创建一个和包同名的目录 urlshort,并将以下两个安装目录复制到这个目录:
google-api-go-client.googlecode.com/hg/urlshortener google-api-go-client.googlecode.com/hg/google-api
此外还要配置下配置文件 app.yaml,内容如下:
application: urlshort version: 0-1-test runtime: go api_version: 3 handlers: - url: /.* script: _go_app
现在你可以去到你的项目目录并在终端运行:dev_appserver.py urlshort
在浏览器打开你的 Web应用:http://localhost:8080。
九、结构(struct)与方法(method)Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。
组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录(Record),比如 Cobol,在 C 家族的编程语言中它也存在,并且名字也是 struct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。不过因为 Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。
1、结构体定义结构体定义的一般方式如下:
type identifier struct { field1 type1 field2 type2 ... }
type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
结构体的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:
var s T s.a = 5 s.b = 8
数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段。
使用 new
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。
var t *T t = new(T)
写这条语句的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(Object)。
示例 structs_fields.go 给出了一个非常简单的例子:
package main import "fmt" type struct1 struct { i1 int f1 float32 str string } func main() { ms := new(struct1) ms.i1 = 10 ms.f1 = 15.5 ms.str= "Chris" fmt.Printf("The int is: %d\n", ms.i1) fmt.Printf("The float is: %f\n", ms.f1) fmt.Printf("The string is: %s\n", ms.str) fmt.Println(ms) }
输出:
The int is: 10 The float is: 15.500000 The string is: Chris &{10 15.5 Chris}
使用 fmt.Println 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value。
同样的,使用点号符可以获取结构体字段的值:structname.fieldname。
在 Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:
type myStruct struct { i int } var v myStruct // v是结构体类型变量 var p *myStruct // p是指向一个结构体类型变量的指针 v.i p.i
初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:
ms := &struct1{10, 15.5, "Chris"} // 此时ms的类型是 *struct1
或者:
var mt struct1 ms := struct1{10, 15.5, "Chris"}
混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。在下面的例子中能看到可以通过在值的前面放上字段名来初始化字段的方式。表达式 new(Type)和 &Type{} 是等价的。
时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子:
type Interval struct { start int end int }
初始化方式:
intr := Interval{0, 3} (A) intr := Interval{end:5, start:1} (B) intr := Interval{end:5} (C)
在(A)中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。(B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。
结构体类型和字段的命名遵循可见性规则,一个导出的结构体类型中有些字段是导出的,另一些不是,这是可能的。
下图说明了结构体类型实例和一个指向它的指针的内存布局:
type Point struct { x, y int }
类型 strcut1 在定义它的包 pack1 中必须是唯一的,它的完全类型名是:pack1.struct1。
下面的例子 Listing 10.2—person.go 显示了一个结构体 Person,一个方法,方法有一个类型为 *Person 的参数(因此对象本身是可以被改变的),以及三种调用这个方法的不同方式:
package main import ( "fmt" "strings" ) type Person struct { firstName string lastName string } func upPerson(p *Person) { p.firstName = strings.ToUpper(p.firstName) p.lastName = strings.ToUpper(p.lastName) } func main() { // 1-struct as a value type: var pers1 Person pers1.firstName = "Chris" pers1.lastName = "Woodward" upPerson(&pers1) fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName) // 2—struct as a pointer: pers2 := new(Person) pers2.firstName = "Chris" pers2.lastName = "Woodward" (*pers2).lastName = "Woodward" // 这是合法的 upPerson(pers2) fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName) // 3—struct as a literal: pers3 := &Person{"Chris","Woodward"} upPerson(pers3) fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName) }
输出:
The name of the person is CHRIS WOODWARD The name of the person is CHRIS WOODWARD The name of the person is CHRIS WOODWARD
在上面例子的第二种情况中,可以直接通过指针,像 pers2.lastName="Woodward" 这样给结构体字段赋值,没有像 C++ 中那样需要使用 -> 操作符,Go 会自动做这样的转换。
注意也可以通过解指针的方式来设置值:(*pers2).lastName = "Woodward"
结构体的内存布局
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。
下面的例子清晰地说明了这些情况:
type Rect1 struct {Min, Max Point } type Rect2 struct {Min, Max *Point }
递归结构体
结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)。如下所示,链表中的 su,树中的 ri 和 le 分别是指向别的节点的指针。
链表:
这块的 data 字段用于存放有效数据(比如 float64),su 指针指向后继节点。
Go 代码:
type Node struct { data float64 su *Node }
链表中的第一个元素叫 head,它指向第二个元素;最后一个元素叫 tail,它没有后继元素,所以它的 su 为 nil 值。当然真实的链接会有很多数据节点,并且链表可以动态增长或收缩。
同样地可以定义一个双向链表,它有一个前趋节点 pr 和一个后继节点 su:
type Node struct { pr *Node data float64 su *Node }
二叉树:
二叉树中每个节点最多能链接至两个节点:左节点(le)和右节点(ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点(root),底层没有子节点的节点叫叶子节点(leaves),叶子节点的 le 和 ri 指针为 nil 值。在 Go 中可以如下定义二叉树:
type Tree strcut { le *Tree data float64 ri *Tree }
结构体转换
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias 类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以如示例 10.3 那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
示例 :
package main import "fmt" type number struct { f float32 } type nr number // alias type func main() { a := number{5.0} b := nr{5.0} // var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment // var i = float32(b) // compile-error: cannot convert b (type nr) to type float32 // var c number = b // compile-error: cannot use b (type nr) as type number in assignment // needs a conversion: var c = number(b) fmt.Println(a, b, c) }
输出:
2、使用工厂方法创建结构体实例{5} {5} {5}
1)结构体工厂
Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂“ 方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。假设定义了如下的 File 结构体类型:
type File struct { fd int // 文件描述符 name string // 文件名 }
下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:
func NewFile(fd int, name string) *File { if fd < 0 { return nil } return &File(fd, name) }
然后这样调用它:
f := NewFile(10, "./test.txt")
在 Go 语言中常常像上面这样在工厂方法里使用初始化来简便的实现构造子。
如果 File 是一个结构体类型,那么表达式 new(File) 和 &File{} 是等价的。
这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:File f = new File(...)。
我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样。
如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})。
如何强制使用工厂方法
通过应用可见性规则就可以禁止使用 new 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。
type matrix struct { ... } func NewMatrix(params) *matrix { m := new(matrix) // 初始化 m return m }
在其他包里使用工厂方法:
package main import "matrix" ... wrong := new(matrix.matrix) // 编译失败(matrix 是私有的) right := matrix.NewMatrix(...) // 实例化 matrix 的唯一方式
2)map 和 struct vs new() 和 make()
现在为止我们已经见到了可以使用 make() 的三种类型中的其中两个:
slices / maps / channels
下面的例子来说明了在映射上使用 new 和 make 的区别,以及可能的发生的错误:
示例new_make.go(不能编译):
package main type Foo map[string]string type Bar struct { thingOne string thingTwo int } func main() { // OK y := new(Bar) (*y).thingOne = "hello" (*y).thingTwo = 1 // NOT OK z := make(Bar) // 编译错误:cannot make type Bar (*y).thingOne = "hello" (*y).thingTwo = 1 // OK x := make(Foo) x["x"] = "goodbye" x["y"] = "world" // NOT OK u := new(Foo) (*u)["x"] = "goodbye" // 运行时错误!! panic: assignment to entry in nil map (*u)["y"] = "world" }
试图 make() 一个结构体变量,会引发一个编译错误,这还不是太糟糕,但是 new() 一个映射并试图使用数据填充它,将会引发运行时错误! 因为 new(Foo) 返回的是一个指向 nil 的指针,它尚未被分配内存。所以在使用 map 时要特别谨慎。
3、使用自定义包中的结构体下面的例子中,main.go 使用了一个结构体,它来自 struct_pack 下的包 structPack。
示例 structPack.go:
package structPack type ExpStruct struct { Mi1 int Mf1 float32 }
示例 main.go:
package main import ( "fmt" "./struct_pack/structPack" ) func main() { struct1 := new(structPack.ExpStruct) struct1.Mi1 = 10 struct1.Mf1 = 16. fmt.Printf("Mi1 = %d\n", struct1.Mi1) fmt.Printf("Mf1 = %f\n", struct1.Mf1) }
输出:
4、带标签的结构体Mi1 = 10 Mf1 = 16.000000
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。我们将在下一章中深入的探讨 reflect包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
示例 struct_tag.go:
package main import ( "fmt" "reflect" ) type TagType struct { // tags field1 bool "An important answer" field2 string "The name of the thing" field3 int "How much there are" } func main() { tt := TagType{true, "Barak Obama", 1} for i := 0; i < 3; i++ { refTag(tt, i) } } func refTag(tt TagType, ix int) { ttType := reflect.TypeOf(tt) ixField := ttType.Field(ix) fmt.Printf("%v\n", ixField.Tag) }
输出:
5、匿名字段和内嵌结构体An important answer The name of the thing How much there are
1)定义
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。
type attr struct{ perm int } type file struct{ name string attr // 仅有类名 } var f file f.name = "lvmenglou" f.perm = 24 // 等价于f.attr.perm = 24
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
考虑如下的程序:
示例 structs_anonymous_fields.go:
package main import "fmt" type innerS struct { in1 int in2 int } type outerS struct { b int c float32 int // anonymous field innerS //anonymous field } func main() { outer := new(outerS) outer.b = 6 outer.c = 7.5 outer.int = 60 outer.in1 = 5 outer.in2 = 10 fmt.Printf("outer.b is: %d\n", outer.b) fmt.Printf("outer.c is: %f\n", outer.c) fmt.Printf("outer.int is: %d\n", outer.int) fmt.Printf("outer.in1 is: %d\n", outer.in1) fmt.Printf("outer.in2 is: %d\n", outer.in2) // 使用结构体字面量 outer2 := outerS{6, 7.5, 60, innerS{5, 10}} fmt.Printf("outer2 is:", outer2) }
输出:
outer.b is: 6 outer.c is: 7.500000 outer.int is: 60 outer.in1 is: 5 outer.in2 is: 10 outer2 is:{6 7.5 60 {5 10}}
通过类型 outer.int 的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。
2)内嵌结构体
同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。外层结构体通过outer.in1 直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。
另外一个例子:
示例 embedd_struct.go:
package main import "fmt" type A struct { ax, ay int } type B struct { A bx, by float32 } func main() { b := B{A{1, 2}, 3.0, 4.0} fmt.Println(b.ax, b.ay, b.bx, b.by) fmt.Println(b.A) }
输出:
1 2 3 4 {1 2}
3)命名冲突
当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?
- 外层名字会覆盖内层名字,这提供了一种重载字段或方法的方式
- 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。
例子:
type A struct {a int} type B struct {a, b int} type C struct {A; B} var c C;
规则 2:使用 c.a 是错误的,到底是 c.A.a 还是 c.B.a 呢?会导致编译器错误:ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a。
type D struct {B; b float32} var d D;
规则1:使用 d.b 是没问题的:它是 float32,而不是 B 的 b。如果想要内层的 b 可以通过 d.B.b 得到。
6、方法1)方法是什么
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型(参考 第 11 章),因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type…。
最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。
一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:
func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix
别名类型不能有它原始类型上已经定义过的方法。
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func 关键字之后的括号中指定 receiver。
如果 recv 是 receiver 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()。
如果 recv 一个指针,Go 会自动解引用。
如果方法不需要使用 recv 的值,可以用 _ 替换它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或self 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:
示例 method .go:
package main import "fmt" type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("The sum is: %d\n", two1.AddThem()) fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20)) two2 := TwoInts{3, 4} fmt.Printf("The sum is: %d\n", two2.AddThem()) } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } func (tn *TwoInts) AddToParam(param int) int { return tn.a + tn.b + param }
输出:
The sum is: 22 Add them to the param: 42 The sum is: 7
下面是非结构体类型上方法的例子:
示例 method2.go:
package main import "fmt" type IntVector []int func (v IntVector) Sum() (s int) { for _, x := range v { s += x } return } func main() { fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6 }
下面这段代码有什么错?
package main import "container/list" func (p *list.List) Iter() { // ... } func main() { lst := new(list.List) for _= range list.Iter() { } }
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误:
cannot define new methods on non-local type int
比如想在 time.Time 上定义如下方法:
func (t time.Time) first3Chars() string { return time.LocalTime().String()[0:3] }
类型在在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误。
但是有一个绕点的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
示例 method_on_time.go:
package main import ( "fmt" "time" ) type myTime struct { time.Time //anonymous field } func (t myTime) first3Chars() string { return t.Time.String()[0:3] } func main() { m := myTime{time.Now()} // 调用匿名Time上的String方法 fmt.Println("Full time now:", m.String()) // 调用myTime.first3Chars fmt.Println("First 3 chars:", m.first3Chars()) } /* Output: Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011 First 3 chars: Mon */
2)函数和方法的区别
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
不要忘记 Method1 后边的括号 (),否则会引发编译器错误:
method recv.Method1 is not an expression, must be called
接收者必须有一个显式的名字,这个名字必须在方法中被使用。
receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。
在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
3) 指针或值作为接收者
鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
下面的例子 pointer_value.go 作了说明:change()接受一个指向 B 的指针,并改变它内部的成员;write() 接受通过拷贝接受 B 的值并只输出B的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是是否在指针上调用方法,Go 替我们做了这些事情。b1 是值而 b2 是指针,方法都支持运行了。
示例 pointer_value.go:
package main import ( "fmt" ) type B struct { thing int } func (b *B) change() { b.thing = 1 } func (b B) write() string { return fmt.Sprint(b) } func main() { var b1 B // b1是值 b1.change() fmt.Println(b1.write()) b2 := new(B) // b2是指针 b2.change() fmt.Println(b2.write()) } /* 输出: {1} {1} */
试着在 write() 中改变接收者b的值:将会看到它可以正常编译,但是开始的 b 没有被改变。
我们知道方法不需要指针作为接收者,如下面的例子,我们只是需要 Point3 的值来做计算:
type Point3 struct { x, y, z float } // A method on Point3 func (p Point3) Abs float { return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z) }
这样做稍微有点昂贵,因为 Point3 是作为值传递给方法的,因此传递的是它的拷贝,这在 Go 中合法的。也可以在指向这个类型的指针上调用此方法(会自动解引用)。
假设 p3 定义为一个指针:p3 := &Point{ 3, 4, 5}。
可以使用 p3.Abs() 来替代 (*p3).Abs()。
像例子(method1.go)中接收者类型是 *TwoInts 的方法 AddThem(),它能在类型 TwoInts 的值上被调用,这是自动间接发生的。
因此 two2.AddThem 可以替代 (&two2).AddThem()。
4)值方法和指针方法
在值和指针上调用方法:可以有连接到类型的方法,也可以有连接到类型指针的方法。
但是这没关系:对于类型 T,如果在 *T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()。
指针方法和值方法都可以在指针或非指针上被调用,如下面程序所示,类型 List 在值上有一个方法 Len(),在指针上有一个方法 Append(),但是可以看到两个方法都可以在两种类型的变量上被调用。
示例 methodset1.go:
package main import ( "fmt" ) type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } func main() { // 值 var lst List lst.Append(1) fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1) // 指针 plst := new(List) plst.Append(2) fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1) }
当你给某个struct定制一个字符串转换方法,可能会纠结选择value methods还是pointer methods方式:
func (ms MyStruct) String() string // value methods func (ms *MyStruct) String() string // pointer methods
在官方effective go文档中,对两者区别描述如下:
-
值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。
-
但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用。
type Foo struct { name string } func (f *Foo) PointerMethod() { fmt.Println("pointer method on", f.name) } func (f Foo) ValueMethod() { fmt.Println("value method on", f.name) } func NewFoo() Foo { // 返回一个右值,不可寻址 return Foo{name: "right value struct"} } func main() { f1 := Foo{name: "value struct"} f1.PointerMethod() // 编译器会自动插入取地址符,变为 (&f1).PointerMethod() f1.ValueMethod() f2 := &Foo{name: "pointer struct"} f2.PointerMethod() f2.ValueMethod() // 编译器会自动解引用,变为 (*f2).PointerMethod() NewFoo().ValueMethod() NewFoo().PointerMethod() // Error!!! }
简而言之,就是不管是普通对象还是指针,都可以调用他们的值方法和指针方法,因为编译器会自行处理,但是对于右值(也就是通过函数返回的临时结构体变量等),只能调用值方法,不能调用指针方法。(备注:对于左值和右值的区别,最重要区别就是是否可以被寻址,可以被寻址的是左值,既可以出现在赋值号左边也可以出现在右边;不可以被寻址的即为右值,比如函数返回值、字面值、常量值等等,只能出现在赋值号右边。)
对于某个特定场景,两者如何取舍其实和另一个问题等价:就是你在定义函数时如何传参——是传值还是传指针。比如上述例子:
func (f *Foo) PointerMethod() { fmt.Println("pointer method on ", f.name) } func (f Foo) ValueMethod() { fmt.Println("value method on", f.name) }
可以转换为下面两个函数进行考虑,用 Go 的术语来说,就是将函数的receiver看做是 argument:
func PointerMethod(f *Foo) { fmt.Println("pointer method on ", f.name) } func ValueMethod(f Foo) { fmt.Println("value method on", f.name) }
在定义receiver为值还是指针时,主要有以下几个考虑点:
-
方法是否需要修改receiver本身。如果需要,那receiver必然要是指针。
-
效率问题。如果receiver是值,那在方法调用时一定会产生 struct 拷贝,而大对象拷贝代价很大。
-
一致性。对于同一个struc 的方法,value method和pointer method混杂用肯定是优雅。
那啥时候用value method呢?很简单的不可变对象使用value method可以减轻gc负担,貌似也就这些好处了。因此请记住:遇事不决请用pointer method!!!
对于第一条,如果需要修改receiver本身,必须用指针,我再举个例子:
type N int func (n N) value() { // value method n++ fmt.Printf("v:%p, %v\n", &n, n) } func (n *N) pointer() { // point method (*n)++ fmt.Printf("p:%p, %v\n", n, *n) } func main() { var a N = 5 a.value() a.pointer() fmt.Printf("a:%p,%v\n", &a, a) }
输出:
v: 0x8200741c8, 26 // 虽然n被打印为26,但是在main()中,n的值还是25,未修改!!! p: 0x8200741c0, 26 a: 0x8200741c0, 26 // n的值被修改为26,是通过a.pointer()修改的!!!
5)方法和未导出字段
考虑 person2.go 中的 person 包:类型 Person 被明确的导出了,但是它的字段没有被导出。例如在 use_person2.go中 p.firsetname 就是错误的。该如何在另一个程序中修改或者只是读取一个 Person 的名字呢?
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只适用成员名。
示例person2.go:
package person type Person struct { firstName string lastName string } func (p *Person) FirstName() string { return p.firstName } func (p *Person) SetFirstName(newName string) { p.firstName = newName }
示例—use_person2.go:
package main import ( "./person" "fmt" ) func main() { p := new(person.Person) // p.firstName undefined // (cannot refer to unexported field or method firstName) // p.firstName = "Eric" p.SetFirstName("Eric") fmt.Println(p.FirstName()) // Output: Eric }
并发访问对象
对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 sync中的方法。
6)内嵌类型的方法和继承当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似 Ruby 中的混入(mixin)。
下面是一个示例:假定有一个 Engine 接口类型,一个 Car 结构体类型,它包含一个Engine 类型的匿名字段:
type Engine interface { Start() Stop() } type Car struct { Engine }
我们可以构建如下的代码:
func (c *Car) GoToWorkIn() { // get in car c.Start() // drive to work c.Stop() // get out of car }
下面是 method3.go 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
package main import ( "fmt" "math" ) type Point struct { x, y float64 } func (p *Point) Abs() float64 { return math.Sqrt(p.x*p.x + p.y*p.y) } type NamedPoint struct { Point name string } func main() { n := &NamedPoint{Point{3, 4}, "Pythagoras"} fmt.Println(n.Abs()) // 打印5 }
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,
可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。
在示例 method4.go 中添加:
func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. }
现在 fmt.Println(n.Abs()) 会打印 500。
因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承,就像:type Child struct { Father; Mother}。
结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。
7)如何在类型中嵌入功能
主要有两种方法来实现在类型中嵌入功能:
A:聚合(或组合):包含一个所需功能类型的具名字段。
B:内嵌:内嵌(匿名地)所需功能类型。
为了使这些概念具体化,假设有一个 Customer 类型,我们想让它通过 Log 类型来包含日志功能,Log 类型只是简单地包含一个累积的消息(当然它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的 Log 类型,然后将它作为特定类型的一个字段,并提供 Log(),它返回这个日志的引用。
方式 A 可以通过如下方法实现:
示例 embed_func1.go:
package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string log *Log } func main() { c := new(Customer) c.Name = "Barak Obama" c.log = new(Log) c.log.msg = "1 - Yes we can!" // shorter c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}} // fmt.Println(c) &{Barak Obama 1 - Yes we can!} c.Log().Add("2 - After me the world will be a better place!") //fmt.Println(c.log) fmt.Println(c.Log()) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) Log() *Log { return c.log }
输出:
1 - Yes we can! 2 - After me the world will be a better place!
相对的方式 B 可能会像这样:
package main import ( "fmt" ) type Log struct { msg string } type Customer struct { Name string Log } func main() { c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}} c.Add("2 - After me the world will be a better place!") fmt.Println(c) } func (l *Log) Add(s string) { l.msg += "\n" + s } func (l *Log) String() string { return l.msg } func (c *Customer) String() string { return c.Name + "\nLog:" + fmt.Sprintln(c.Log) }
输出:
Barak Obama Log:{1 - Yes we can! 2 - After me the world will be a better place!}
内嵌的类型不需要指针,Customer 也不需要 Add 方法,它使用 Log 的 Add 方法,Customer 有自己的 String 方法,并且在它里面调用了 Log 的 String 方法。
如果内嵌类型嵌入了其他类型,也是可以的,那些类型的方法可以直接在外层类型中使用。
因此一个好的策略是创建一些小的、可复用的类型作为一个工具箱,用于组成域类型。
8)多重继承
多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。
作为一个例子,假设有一个类型 CameraPhone,通过它可以 Call(),也可以 TakeAPicture(),但是第一个方法属于类型 Phone,第二个方法属于类型 Camera。
只要嵌入这两个类型就可以解个问题,如下所示:
package main import ( "fmt" ) type Camera struct{} func (c *Camera) TakeAPicture() string { return "Click" } type Phone struct{} func (p *Phone) Call() string { return "Ring Ring" } type CameraPhone struct { Camera Phone } func main() { cp := new(CameraPhone) fmt.Println("Our new CameraPhone exhibits multiple behaviors...") fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture()) fmt.Println("It works like a Phone too: ", cp.Call()) }
输出:
Our new CameraPhone exhibits multiple behaviors... It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring
9)通用方法和方法命名
在编程中一些基本操作会一遍又一遍的出现,比如打开(Open)、关闭(Close)、读(Read)、写(Write)、排序(Sort)等等,并且它们都有一个大致的意思:打开(Open)可以作用于一个文件、一个网络连接、一个数据库连接等等。具体的实现可能千差万别,但是基本的概念是一致的。
在 Go 语言中,通过使用接口,标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 Open()、Read()、Write()等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样。这样做会使 Go 开发的软件更加具有一致性和可读性。比如:如果需要一个 convert-to-string 方法,应该命名为 String(),而不是 ToString()。
10)和其他面向对象语言比较 Go 的类型和方法
在如 C++、Java、C# 和 Ruby 这样的面向对象语言中,方法在类的上下文中被定义和继承:在一个对象上调用方法时,运行时会检测类以及它的超类中是否有此方法的定义,如果没有会导致异常发生。
在 Go 语言中,这样的继承层次是完全没必要的:如果方法在此类型定义了,就可以调用它,和其他类型上是否存在这个方法没有关系。在这个意义上,Go 具有更大的灵活性。
下面的模式就很好的说明了这个问题:
Go 不需要一个显式的类定义,如同 Java、C++、C# 等那样,相反地,“类”是通过提供一组作用于一个共同类型的方法集来隐式定义的。类型可以是结构体或者任何用户自定义类型。
比如:我们想定义自己的
Integer
类型,并添加一些类似转换成字符串的方法,在 Go 中可以如下定义:type Integer int func (i *Integer) String() string { return strconv.Itoa(i) }
在 Java 或 C# 中,这个方法需要和类 Integer 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。
11)方法覆盖
方法存在同名遮蔽问题,通过匿名字段user构建manager结构,两者都实现了toString()方法,对于变量m调用toString()方法时,会直接屏蔽掉user.toString()方法,实现覆盖操作;但是也可以通过m.user.toString()直接访问的user的toString()方法。
type user struct{} type manager struct{ user // 匿名字段 } func (user) toString() string{ return "user" } func (m manager) toString() string{ return m.user.toString() + ";manager" } func main(){ var m manager println(m.toString()) // 输出:user;manager println(m.user.toString()) // 输出:user }
12)方法集
所谓“方法集”,简单来说,就是对于普通类型和指针类型,所包含的方法集合,我们先看个例子:
type S struct{} type T struct{ S // 匿名嵌入字段 } func (S) sVal() {} func (*S) sPtr() {} func (T) sVal() {} func (*T) sPtr() {} func methodSet(a interface()) { t := reflect.TypeOf(a) for i, n := 0, t.NumMethod(); i < n; i++ { m := t.Method(i) fmt.Println(m.Name, m.Type) } } func main() { var t T methodSet(t) // 显示T方法集 println("---------------") methodSet(&t) // 显示*T方法集 }
输出:
sVal func(main.T) tVal func(main.T) ---------------- sPtr func(*main.T) sVal func(*main.T) tPtr func(*main.T) tVal func(*main.T)
可以看出,普通类型的方法集,是值方法;指针类型的方法集,是值方法 + 指针方法,更多情况可总结如下:
-
类型T方法集包含所有的receive T方法;
-
类型*T方法集包含所有receive T + *T方法;
-
匿名嵌入S,T方法集包含所有receive T方法;
-
匿名嵌入*S,T方法集包含所有receive S + *S方法;
-
匿名嵌入S或*S,*T方法集包含所有receive S + *S方法。
总结
在 Go 中,类型就是类(数据和关联的方法)。Go 不知道类似面向对象语言的类继承的概念。继承有两个好处:代码复用和多态。
在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫组件编程。
许多开发者说相比于类继承,Go 的接口提供了更强大、却更简单的多态行为。
“匿名字段”用的非常多,它是其声明中只有类型而没有名称的字段,可以以一种很自然的方式为被嵌入的类型带来新的属性和能力。不过,我们需要小心可能产生“屏蔽”现象的地方,尤其是当存在多个嵌入字段或者多层嵌入的时候,“屏蔽”现象可能会让你的实际引用与你的预期不符。
另外,你一定要梳理清楚值方法和指针方法的不同之处,包括这两种方法各自能做什么、不能做什么以及会影响到其所属类型的哪些方面。这涉及值的修改、方法集合和接口实现。再次强调,嵌入字段是实现类型间组合的一种方式,这与继承没有半点儿关系。Go 语言虽然支持面向对象编程,但是根本就没有“继承”这个概念。
最后就是“方法集”,对于普通类型和指针类型,当存在匿名字段时,他们的值方法和指针方法的所属关系,这个是需要掌握的,这里我们没有讨论实际的应用场景,但是当涉及到接口实现和类型转换时,如果对这块知识掌握不够的话,转换会非常容易出现问题。对于方法表达式这块,这块知识不容易和其它知识混淆,也不难掌握,大家可以自行查阅相关资料即可。
备注
如果真的需要更多面向对象的能力,看一下 goop 包(Go Object-Oriented Programming),它由 Scott Pakin 编写: 它给 Go 提供了 JavaScript 风格的对象(基于原型的对象),并且支持多重继承和类型独立分派,通过它可以实现你喜欢的其他编程语言里的一些结构。
7、垃圾回收和 SetFinalizerGo 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器(GC),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 runtime 包访问 GC 进程。
通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用runtime.GC(),它会此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 GC 进程在执行)。
如果想知道当前的内存状态,可以使用:
fmt.Printf("%d\n", runtime.MemStats.Alloc/1024)
上面的程序会给出已分配内存的总量,单位是 Kb。进一步的测量参考 文档页面。
如果需要在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现:
runtime.SetFinalizer(obj, func(obj *typeObj))
func(obj *typeObj) 需要一个 typeObj 类型的指针参数 obj,特殊操作会在它上面执行。func 也可以是一个匿名函数。
在对象被 GC 进程选中并从内存中移除以前,SetFinalizer 都不会执行,即使程序正常结束或者发生错误。
十、接口(interface)与反射(reflection) 1、接口定义Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:
type Namer interface { Method1(param_list) return_type Method2(param_list) return_type ... }
上面的Namer是一个接口类型。
(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法,例如:
type tester interface { test() string() string } type data struct {} func (*data) test() {} func (data) string() string { return "" } func main() { var d data // var t tester = d // 错误:test()不属于data的方法集,所以不支持转换 var t tester = &d // 结构类型是可以直接转换为接口类型 t.test() println(t.string()) }
编译器是根据方法集来判断是否实现了接口,显然再上例中只有*data才符合tester的要求,所以不支持var t tester = d的转换,但支持var t tester = &d。可能有同学会觉得这个也和容易,那么当和“嵌入字段”结合起来,这里的转换逻辑可能就比较复杂了,因为只有相同的“方法集”(我再强调一下,方法集必须是包含或者相等关系,比如A必须包含B所有的方法集,A才能赋值给B),才能进行换行,具体的包含关系规则。
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个接口值 :var ai Namer,ai是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
类型(比如结构体)实现接口方法集中的方法,每一个方法的实现说明了此方法是如何作用于该类型的:即实现接口,同时方法集也构成了该类型的接口。实现了 Namer 接口类型的变量可以赋值给 ai (接收者值),此时方法表中的指针会指向被实现的接口方法。当然如果另一个类型(也实现了该接口)的变量被赋值给 ai,这二者(指针和方法实现)也会随之改变。
类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
一个类型可以实现多个接口。
接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。
所有这些特性使得接口具有很大的灵活性。
第一个例子:
示例nterfaces.go:
package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { sq1 := new(Square) sq1.side = 5 // var areaIntf Shaper // areaIntf = sq1 // shorter,without separate declaration: // areaIntf := Shaper(sq1) // or even: areaIntf := sq1 fmt.Printf("The square has area: %f\n", areaIntf.Area()) }
输出:
The square has area: 25.000000
上面的程序定义了一个结构体 Square 和一个接口 Shaper,接口有一个方法 Area()。
在 main() 方法中创建了一个 Square 的实例。在主程序外边定义了一个接收者类型是 Square 方法的 Area(),用来计算正方形的面积:结构体 Square 实现了接口 Shaper 。
所以可以将一个 Square 类型的变量赋值给一个接口类型的变量:areaIntf = sq1 。
现在接口变量包含一个指向 Square 变量的引用,通过它可以调用 Square 上的方法 Area()。当然也可以直接在Square 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。
这是 多态 的 Go 版本,多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。
如果 Square 没有实现 Area() 方法,编译器将会给出清晰的错误信息:
cannot use sq1 (type *Square) as type Shaper in assignment: *Square does not implement Shaper (missing Area method)
如果 Shaper 有另外一个方法 Perimeter(),但是Square 没有实现它,即使没有人在 Square 实例上调用这个方法,编译器也会给出上面同样的错误。
扩展一下上面的例子,类型 Rectangle 也实现了 Shaper 接口。接着创建一个 Shaper 类型的数组,迭代它的每一个元素并在上面调用 Area() 方法,以此来展示多态行为:
示例interfaces_poly.go:
package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } type Rectangle struct { length, width float32 } func (r Rectangle) Area() float32 { return r.length * r.width } func main() { r := Rectangle{5, 3} // Area() of Rectangle needs a value q := &Square{5} // Area() of Square needs a pointer // shapes := []Shaper{Shaper(r), Shaper(q)} // or shorter shapes := []Shaper{r, q} fmt.Println("Looping through shapes for area ...") for n, _ := range shapes { fmt.Println("Shape details: ", shapes[n]) fmt.Println("Area of this shape is: ", shapes[n].Area()) } }
输出:
Looping through shapes for area ... Shape details: {5 3} Area of this shape is: 15 Shape details: &{5} Area of this shape is: 25
在调用 shapes[n].Area()) 这个时,只知道 shapes[n] 是一个 Shaper 对象,最后它摇身一变成为了一个 Square 或Rectangle 对象,并且表现出了相对应的行为。
也许从现在开始你将看到通过接口如何产生 更干净、更简单 及 更具有扩展性 的代码。在 11.12.3 中将看到在开发中为类型添加新的接口是多么的容易。
下面是一个更具体的例子:有两个类型 stockPosition 和 car,它们都有一个 getValue() 方法,我们可以定义一个具有此方法的接口 valuable。接着定义一个使用 valuable 类型作为参数的函数 showValue(),所有实现了 valuable接口的类型都可以用这个函数。
示例 valuable.go:
package main import "fmt" type stockPosition struct { ticker string sharePrice float32 count float32 } /* method to determine the value of a stock position */ func (s stockPosition) getValue() float32 { return s.sharePrice * s.count } type car struct { make string model string price float32 } /* method to determine the value of a car */ func (c car) getValue() float32 { return c.price } /* contract that defines different things that have value */ type valuable interface { getValue() float32 } func showValue(asset valuable) { fmt.Printf("Value of the asset is %f\n", asset.getValue()) } func main() { var o valuable = stockPosition{"GOOG", 577.20, 4} showValue(o) o = car{"BMW", "M3", 66500} showValue(o) }
输出:
Value of the asset is 2308.800049 Value of the asset is 66500.000000
一个标准库的例子
io 包里有一个接口类型 Reader:
type Reader interface { Read(p []byte) (n int, err error) }
定义变量 r:var r io.Reader
那么就可以写如下的代码:
var r io.Reader r = os.Stdin // see 12.1 r = bufio.NewReader(r) r = new(bytes.Buffer) f,_ := os.Open("test.txt") r = bufio.NewReader(f)
上面 r 右边的类型都实现了 Read() 方法,并且有相同的方法签名,r 的静态类型是 io.Reader。
备注
有的时候,也会以一种稍微不同的方式来使用接口这个词:从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外定一个接口而已。
2、内嵌接口一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。
type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() }
接口类型间的嵌入也被称为接口的组合,这个我们很容易联想到结构体类型的嵌入字段,但是两者有很大不同,接口类型间的嵌入要更简单一些,因为它不会涉及方法间的“屏蔽”,只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译。所以接口体类型组合会存在方法“屏蔽”现象,但是接口不会,这个是两者非常重要的区别!下面我们看一个接口组合的示例:
type stringer interface { string() string } type tester interface { stringer // 嵌入stringer接口 tester() } type data struct {} func (*data) test() {} func (data) string() string { return "" } func pp(a stringer) { // 超级接口变量,可以隐式转换为子集,反过来不行 println(a.string()) } func main() { var d data var t tester = &d // *data包含tester所有的方法集,实现了tester接口 pp(t) // 隐式转换为接口子集stringer var s stringer = t // 显示转换为接口子集stringer println(s.string()) // var t2 tester = s // 接口不能逆向转换 }
通过上面的示例,我们可以得出一个结论:接口变量可显式/隐式转换为子集,但是不能逆向转换。然后上面的转换过程中,还是绕不开“方法集”的概念,比如写成var t tester = d就不行了,这个概念我再给大家强化一下。
我们可以通过结构体内嵌结构体,实现“匿名字段”和“方法覆盖”。也可以通过接口内嵌接口,也就是“接口组合”。但是对于结构体内嵌结构的使用,这个可能遇到的比较少:
type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } type reverse struct { Interface }
大家可能对这用用法比较疑惑,不知道这种方法具体的使用场景是什么?下面我们来看一个完整的例子,以下代码是从sort包提取出来的:
type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } // Array 实现Interface接口 type Array []int func (arr Array) Len() int { return len(arr) } func (arr Array) Less(i, j int) bool { return arr[i] < arr[j] } func (arr Array) Swap(i, j int) { arr[i], arr[j] = arr[j], arr[i] } // 匿名接口(anonymous interface) type reverse struct { Interface } // 重写(override) func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } // 构造reverse Interface func Reverse(data Interface) Interface { return &reverse{data} } func main() { arr := Array{1, 2, 3} rarr := Reverse(arr) fmt.Println(arr.Less(0,1)) fmt.Println(rarr.Less(0,1)) }
sort包中这么写的目的是为了重写Interface的Less方法,并有效利用了原始的Less方法;通过Reverse可以从Interface构造出一个反向的Interface。go语言利用组合的特性,寥寥几行代码就实现了重写。对比一下传统的组合匿名结构体实现重写的写法,或许可以更好的帮助我们了解匿名接口的优点:
// 同上,全部省略。。。 // 匿名struct type reverse struct { Array } // 重写 func (r reverse) Less(i, j int) bool { return r.Array.Less(j, i) } // 构造reverse Interface func Reverse(data Array) Interface { return &reverse{data} } func main() { arr := Array{1, 2, 3} rarr := Reverse(arr) fmt.Println(arr.Less(0, 1)) fmt.Println(rarr.Less(0, 1)) }
上面这个例子使用了匿名结构体的写法,和之前匿名接口的写法实现了同样的重写功能,甚至非常相似。但是仔细对比一下你就会发现匿名接口的优点,匿名接口的方式不依赖具体实现,可以对任意实现了该接口的类型进行重写。这在写一些公共库时会非常有用,如果你经常看一些库的源码,匿名接口的写法应该会很眼熟。这里我总结结构体内嵌接口的作用:
-
不依赖具体实现:即接口为A,结构体B1、B2实现了接口A,结构体C内嵌了A,那么C.A可以通过B1/B2实例化;
-
对接口类型进行重写:当C.A通过B1实例化后,C和B1的关系,可以转变为接口体C内嵌结构体B1,那么C可以直接使用B1中的所有方法,当然C也可以对B1中的方法进行重写,这里官方文档这样解释“Interface and we can override a specific method without having to define all the others.”
对于一个接口类型的变量来说,我们赋给它的值可以被叫做它的实际值(也称动态值),而该值的类型可以被叫做这个变量的实际类型(也称动态类型)。也就是说,一个接口类型的值(简称为接口值)其实有两个部分:动态类型 + 动态值,下面看一个示例:
type Pet interface { Name() string } type Dog struct { Language() string } type Cat struct { Color() string } func (d Dog) Name() string { return "Dog" } func (d Dog) Language() string { return "汪汪汪" } func (d Cat) Name() string { return "Cat" } func (d Cat) Color() string { return "Black" } func main() { var p pet var dog1 Dog var cat1 Cat pet = dog1 // 动态类型为Dog println(pet.Name()) // 输出:Dog,其实调用的是dog.Name() pet = cat1 // 动态类型为Cat println(pet.Name()) // 输出:Cat,其实调用的是cat.Name() pet = nil // 动态类型和值都是nil }
动态类型这个叫法是相对于静态类型而言的,对于变量pet来讲,它的静态类型就是Pet,并且永远是Pet,但是它的动态类型却会随着我们赋给它的动态值而变化。所以执行pet = dog1时,pet的动态类型为Dog,动态值是dog1的副本;执行pet = cat1时,pet的动态类型为Cat,动态值是cat1的副本。我们可以通过Go语言的这个特性,来实现C++中多态的方法特性。
如何检测和转换接口变量的类型?
一个接口类型的变量
varI
中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用类型断言 来测试在某个时刻varI
是否包含类型T
的值:v := varI.(T) // unchecked type assertion
varI 必须是一个接口变量,否则编译器会报错:
invalid type assertion: varI.(T) (non-interface type (type of varI) on left) 。
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
if v, ok := varI.(T); ok { // checked type assertion Process(v) return } // varI is not of type T
如果转换合法,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。
应该总是使用上面的方式来进行类型断言。
多数情况下,我们可能只是想在 if 中测试一下 ok 的值,此时使用以下的方法会是最方便的:
if _, ok := varI.(T); ok { // ... }
示例 type_interfaces.go:
package main import ( "fmt" "math" ) type Square struct { side float32 } type Circle struct { radius float32 } type Shaper interface { Area() float32 } func main() { var areaIntf Shaper sq1 := new(Square) sq1.side = 5 areaIntf = sq1 // Is Square the type of areaIntf? if t, ok := areaIntf.(*Square); ok { fmt.Printf("The type of areaIntf is: %T\n", t) } if u, ok := areaIntf.(*Circle); ok { fmt.Printf("The type of areaIntf is: %T\n", u) } else { fmt.Println("areaIntf does not contain a variable of type Circle") } } func (sq *Square) Area() float32 { return sq.side * sq.side } func (ci *Circle) Area() float32 { return ci.radius * ci.radius * math.Pi }
输出:
The type of areaIntf is: *main.Square areaIntf does not contain a variable of type Circle
程序行中定义了一个新类型 Circle,它也实现了 Shaper 接口。 t, ok := areaIntf.(*Square); ok 测试 areaIntf里是否一个包含 'Square' 类型的变量,结果是确定的;然后我们测试它是否包含一个 'Circle' 类型的变量,结果是否定的。
备注
如果忽略 areaIntf.(*Square) 中的 * 号,会导致编译错误:impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)。
4、类型判断:type-switch接口变量的类型也可以使用一种特殊形式的
swtich
来检测:type-swtich:switch t := areaIntf.(type) { case *Square: fmt.Printf("Type Square %T with value %v\n", t, t) case *Circle: fmt.Printf("Type Circle %T with value %v\n", t, t) case nil: fmt.Printf("nil value: nothing to check?\n") default: fmt.Printf("Unexpected type %T\n", t) }
输出:
Type Square *main.Square with value &{5}
变量 t 得到了 areaIntf 的值和类型, 所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口(在上例中即 Shaper),如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。
可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。
如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:
switch areaIntf.(type) { case *Square: // TODO case *Circle: // TODO ... default: // TODO }
下面的代码片段展示了一个类型分类函数,它有一个可变长度参数,可以是任意类型的数组,它会根据数组元素的实际类型执行不同的动作:
func classifier(items ...interface{}) { for i, x := range items { switch x.(type) { case bool: fmt.Printf("Param #%d is a bool\n", i) case float64: fmt.Printf("Param #%d is a float64\n", i) case int, int64: fmt.Printf("Param #%d is a int\n", i) case nil: fmt.Printf("Param #%d is a nil\n", i) case string: fmt.Printf("Param #%d is a string\n", i) default: fmt.Printf("Param #%d is unknown\n", i) } } }
可以这样调用此方法:classifier(13, -14.3, "BELGIUM", complex(1, 2), nil, false) 。
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
在示例(xml.go)中解析 XML 文档时,我们就会用到 type-switch。
5、测试一个值是否实现了某个接口这是类型断言中的一个特例:假定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口,可以这样做:
type Stringer interface { String() string } if sv, ok := v.(Stringer); ok { fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v }
Print 函数就是如此检测类型是否可以打印自身的。
接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
编写参数是接口变量的函数,这使得它们更具有一般性。
使用接口使代码更具有普适性。
标准库里到处都使用了这个原则,如果对接口概念没有良好的把握,是不可能理解它是如何构建的。
在接下来的章节中,我们会讨论两个重要的例子,试着去深入理解它们,这样你就可以更好的应用上面的原则。
6、使用方法集与接口作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。考虑下面的程序:
示例methodset2.go:
package main import ( "fmt" ) type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } type Appender interface { Append(int) } func CountInto(a Appender, start, end int) { for i := start; i 42 } func main() { // A bare value var lst List // compiler error: // cannot use lst (type List) as type Appender in argument to CountInto: // List does not implement Appender (Append method has pointer receiver) // CountInto(lst, 1, 10) if LongEnough(lst) { // VALID:Identical receiver type fmt.Printf("- lst is long enough\n") } // A pointer value plst := new(List) CountInto(plst, 1, 10) //VALID:Identical receiver type if LongEnough(plst) { // VALID: a *List can be dereferenced for the receiver fmt.Printf("- plst is long enough\n") } }
讨论
在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的因为 'Len' 定义在值上。
在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。
总结
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型
P
直接可以辨识的:- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
将一个值赋值给一个接口赋值时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
Go 语言规范定义了接口方法集的调用规则:
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
1)第一个例子:使用 Sorter 接口排序
一个很好的例子是来自标准库的 sort 包,要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的 Len()方法、比较第 i 和 j 个元素的 Less(i, j) 方法以及交换第 i 和 j 个元素的 Swap(i, j) 方法。
排序函数的算法只会使用到这三个方法(可以使用任何排序算法来实现,此处我们使用冒泡排序):
func Sort(data Sorter) { for pass := 1; pass < data.Len(); pass++ { for i := 0;i < data.Len() - pass; i++ { if data.Less(i+1, i) { data.Swap(i, i + 1) } } } }
Sort 函数接收一个接口类型参数:Sorter ,它声明了这些方法:
type Sorter interface { Len() int Less(i, j int) bool Swap(i, j int) }
参数中的 int 不是说要排序的对象一定要是一组 int、i 和 j 表示元素的整型索引,长度也是整型的。
现在如果我们想对一个 int 数组进行排序,所有必须做的事情就是:为数组定一个类型并在它上面实现 Sorter 接口的方法:
type IntArray []int func (p IntArray) Len() int { return len(p) } func (p IntArray) Less(i, j int) bool { return p[i] < p[j] } func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
下面是调用排序函数的一个具体例子:
data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586} a := sort.IntArray(data) //conversion to type IntArray from package sort sort.Sort(a)
完整的、可运行的代码可以在 sort.go 和 sortmain.go 里找到。
同样的原理,排序函数可以用于一个浮点型数组,一个字符串数组,或者一个表示每周各天的结构体 dayArray.
示例sort.go:
package sort type Sorter interface { Len() int Less(i, j int) bool Swap(i, j int) } func Sort(data Sorter) { for pass := 1; pass < data.Len(); pass++ { for i := 0; i < data.Len()-pass; i++ { if data.Less(i+1, i) { data.Swap(i, i+1) } } } } func IsSorted(data Sorter) bool { n := data.Len() for i := n - 1; i > 0; i-- { if data.Less(i, i-1) { return false } } return true } // Convenience types for common cases type IntArray []int func (p IntArray) Len() int { return len(p) } func (p IntArray) Less(i, j int) bool { return p[i] < p[j] } func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] } type StringArray []string func (p StringArray) Len() int { return len(p) } func (p StringArray) Less(i, j int) bool { return p[i] < p[j] } func (p StringArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // Convenience wrappers for common cases func SortInts(a []int) { Sort(IntArray(a)) } func SortStrings(a []string) { Sort(StringArray(a)) } func IntsAreSorted(a []int) bool { return IsSorted(IntArray(a)) } func StringsAreSorted(a []string) bool { return IsSorted(StringArray(a)) }
示例 sortmain.go:
package main import ( "./sort" "fmt" ) func ints() { data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586} a := sort.IntArray(data) //conversion to type IntArray sort.Sort(a) if !sort.IsSorted(a) { panic("fails") } fmt.Printf("The sorted array is: %v\n", a) } func strings() { data := []string{"monday", "friday", "tuesday", "wednesday", "sunday", "thursday", "", "saturday"} a := sort.StringArray(data) sort.Sort(a) if !sort.IsSorted(a) { panic("fail") } fmt.Printf("The sorted array is: %v\n", a) } type day struct { num int shortName string longName string } type dayArray struct { data []*day } func (p *dayArray) Len() int { return len(p.data) } func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num } func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] } func days() { Sunday := day{0, "SUN", "Sunday"} Monday := day{1, "MON", "Monday"} Tuesday := day{2, "TUE", "Tuesday"} Wednesday := day{3, "WED", "Wednesday"} Thursday := day{4, "THU", "Thursday"} Friday := day{5, "FRI", "Friday"} Saturday := day{6, "SAT", "Saturday"} data := []*day{&Tuesday, &Thursday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday} a := dayArray{data} sort.Sort(&a) if !sort.IsSorted(&a) { panic("fail") } for _, d := range data { fmt.Printf("%s ", d.longName) } fmt.Printf("\n") } func main() { ints() strings() days() }
输出:
The sorted array is: [-5467984 -784 0 0 42 59 74 238 905 959 7586 7586 9845] The sorted array is: [ friday monday saturday sunday thursday tuesday wednesday] Sunday Monday Tuesday Wednesday Thursday Friday Saturday
备注:
panic("fail") 用于停止处于在非正常情况下的程序(详细请参考 第13章),当然也可以先打印一条信息,然后调用os.Exit(1) 来停止程序。
上面的例子帮助我们进一步了解了接口的意义和使用方式。对于基本类型的排序,标准库已经提供了相关的排序函数,所以不需要我们再重复造轮子了。对于一般性的排序,sort 包定义了一个接口:
type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) }
这个接口总结了需要用于排序的抽象方法,函数 Sort(data Interface) 用来对此类对象进行排序,可以用它们来实现对其他数据(非基本类型)进行排序。在上面的例子中,我们也是这么做的,不仅可以对 int 和 string 序列进行排序,也可以对用户自定义类型 dayArray 进行排序。
2)第二个例子:读和写
读和写是软件中很普遍的行为,提起它们会立即想到读写文件、缓存(比如字节或字符串切片)、标准输入输出、标准错误以及网络连接、管道等等,或者读写我们的自定义类型。为了是代码尽可能通用,Go 采取了一致的方式来读写数据。
io 包提供了用于读和写的接口 io.Reader 和 io.Writer:
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
只要类型实现了读写接口,提供 Read() 和 Write 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 io.Reader 接口,这个接口只有一个签名是 Read(p []byte) (n int, err error) 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 error 对象,如果没有错误发生返回 'nil',如果已经到达输入的尾端,会返回 io.EOF("EOF"),如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 io.Writer 接口,这个接口也只有一个签名是 Write(p []byte) (n int, err error) 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数一个 error 对象(如果没有错误发生就是 nil)。
io 包里的 Readers 和 Writers 都是不带缓冲的,bufio 包里提供了对应的带缓冲的操作,在读写 UTF-8 编码的文本文件时它们尤其有用。在 第12章 我们会看在实战使用它们的很多例子。
在实际编程中尽可能的使用这些接口,会使程序变得更通用,可以在任何实现了这些接口的类型上使用读写方法。
例如一个 JPEG 图形解码器,通过一个 Reader 参数,它可以解码来自磁盘、网络连接或以 gzip 压缩的 HTTP 流中的JPEG 图形数据,或者其他任何实现了 Reader 接口的对象。
7、空接口1)空接口定义
空接口或者最小接口 不包含任何方法,它对实现不做任何要求:
type Any interface {}
任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。
空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。
可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。
示例 empty_interface.go:
package main import "fmt" var i = 5 var str = "ABC" type Person struct { name string age int } type Any interface{} func main() { var val Any val = 5 fmt.Printf("val has the value: %v\n", val) val = str fmt.Printf("val has the value: %v\n", val) pers1 := new(Person) pers1.name = "Rob Pike" pers1.age = 55 val = pers1 fmt.Printf("val has the value: %v\n", val) switch t := val.(type) { case int: fmt.Printf("Type int %T\n", t) case string: fmt.Printf("Type string %T\n", t) case bool: fmt.Printf("Type boolean %T\n", t) case *Person: fmt.Printf("Type pointer to Person %T\n", t) default: fmt.Printf("Unexpected type %T", t) } }
输出:
val has the value: 5 val has the value: ABC val has the value: &{Rob Pike 55} Type pointer to Person *main.Person
在上面的例子中,接口变量 val 被依次赋予一个 int,string 和 Person 实例的值,然后使用 type-swtich 来测试它的实际类型。每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。
例子 emptyint_switch.go 说明了空接口在 type-swtich 中联合 lambda 函数的用法:
package main import "fmt" type specialString string var whatIsThis specialString = "hello" func TypeSwitch() { testFunc := func(any interface{}) { switch v := any.(type) { case bool: fmt.Printf("any %v is a bool type", v) case int: fmt.Printf("any %v is an int type", v) case float32: fmt.Printf("any %v is a float32 type", v) case string: fmt.Printf("any %v is a string type", v) case specialString: fmt.Printf("any %v is a special String!", v) default: fmt.Println("unknown type!") } } testFunc(whatIsThis) } func main() { TypeSwitch() }
输出:
any hello is a special String!
2)构建通用类型或包含不同类型变量的数组
我们看到了能被搜索和排序的 int 数组、float 数组以及 string 数组,那么对于其他类型的数组呢,是不是我们必须得自己编程实现它们?
现在我们知道该怎么做了,就是通过使用空接口。让我们给空接口定一个别名类型 Element:type Element interface{}
然后定义一个容器类型的结构体 Vector,它包含一个 Element 类型元素的切片:
type Vector struct { a []Element }
Vector 里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回第 i 个元素:
func (p *Vector) At(i int) Element { return p.a[i] }
再定一个 Set() 方法用于设置第 i 个元素的值:
func (p *Vector) Set(i int, e Element) { p.a[i] = e }
Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。
3)复制数据切片至空接口切片
假设你有一个
myType
类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:var dataSlice []myType = FuncReturnSlice() var interfaceSlice []interface{} = dataSlice
可惜不能这么做,编译时会出错:
cannot use dataSlice (type []myType) as type []interface { } in assignment
原因是它们俩在内存中的布局是不一样的(参考 官方说明)。
必须使用 for-range 语句来一个一个显式地复制:
var dataSlice []myType = FuncReturnSlice() var interfaceSlice []interface{} = make([]interface{}, len(dataSlice)) for ix, d := range dataSlice { interfaceSlice[i] = d }
4)通用类型的节点数据结构
我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode 方法,及设置数据的 SetData 方法.
示例 node_structures.go:
package main import "fmt" type Node struct { le *Node data interface{} ri *Node } func NewNode(left, right *Node) *Node { return &Node{left, nil, right} } func (n *Node) SetData(data interface{}) { n.data = data } func main() { root := NewNode(nil, nil) root.SetData("root node") // make child (leaf) nodes: a := NewNode(nil, nil) a.SetData("left node") b := NewNode(nil, nil) b.SetData("right node") root.le = a root.ri = b fmt.Printf("%v\n", root) // Output: &{0x125275f0 root node 0x125275e0} }
5)接口到接口
一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 'Go' 语言动态的一面,可以那它和 Ruby 和 Python 这些动态语言相比较。
假定:
var ai AbsInterface // declares method Abs() type SqrInterface interface { Sqr() float } var si SqrInterface pp := new(Point) // say *Point implements Abs, Sqr var empty interface{}
那么下面的语句和类型断言是合法的:
empty = pp // everything satisfies empty ai = empty.(AbsInterface) // underlying value pp implements Abs() // (runtime failure otherwise) si = ai.(SqrInterface) // *Point has Sqr() even though AbsInterface doesn’t empty = si // *Point implements empty set // Note: statically checkable so type assertion not necessary.
下面是函数调用的一个例子:
type myPrintInterface interface { print() } func f3(x myInterface) { x.(myPrintInterface).print() // type assertion to myPrintInterface }
x 转换为 myPrintInterface 类型是完全动态的:只要 x 的底层类型(动态类型)定义了 print 方法这个调用就可以正常运行。
8、反射包1)方法和类型的反射
我们看到可以通过反射来分析一个结构体。本节我们进一步探讨强大的反射功能。反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和动态的调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
变量的最基本信息就是类型和值:反射包的Type用来表示一个Go类型,反射包的Value为Go值提供了反射接口。
两个简单的函数,reflect.TypeOf和reflect.ValueOf,返回被检查对象的类型和值。例如,x被定义为:var x float64 = 3.4,那么reflect.TypeOf(x)返回float64,reflect.ValueOf(x)返回
实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:
func TypeOf(i interface{}) Type func ValueOf(i interface{}) Value
接口的值包含一个type和value.
反射可以从接口值反射到对象,也可以从对象反射回接口值。
reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样)
const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
对于变量x,如果v:=reflect.ValueOf(x)那么v.Kind()返回float64,所以下面的表达式是truev.Kind() == reflect.Float64
Kind总是返回底层类型:
type MyInt int var m MyInt = 5 v := reflect.ValueOf(m)
v.Kind()返回reflect.Int
Interface()方法还原(接口)值的值,所以要打印v的值:fmt.Println(v.Interface())
尝试运行下面的代码:
示例 reflect1.go:
// blog: Laws of Reflection package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) v := reflect.ValueOf(x) fmt.Println("value:", v) fmt.Println("type:", v.Type()) fmt.Println("kind:", v.Kind()) fmt.Println("value:", v.Float()) fmt.Println(v.Interface()) fmt.Printf("value is %5.2e\n", v.Interface()) y := v.Interface().(float64) fmt.Println(y) } /* output: type: float64 value: type: float64 kind: float64 value: 3.4 3.4 value is 3.40e+00 3.4 */
知道x是一个float64类型的值,reflect.ValueOf(x).float()返回这个float64类型的实际值;同样的适用于Int()、 Bool()、 Complex() 、String()。
2)通过反射修改(设置)值
继续前面的例子(参阅 reflect2.go),假设我们把x的值改为3.1415。Value有一些方法可以完成这个任务,但是必须小心使用:v.SetFloat(3.1415)
这将产生一个错误: will panic: reflect.Value.SetFloat using unaddressable value
为什么会这样呢?问题的原因是v不是可设置的(这里并不是说值不可寻址)。是否可设置是Value的一个属性,并且不是所有的反设值都有这个属性:可以使用CanSet()方法测试是否可设置。
在例子中我们看到v.CanSet()返回false: settability of v: false
当v := reflect.ValueOf(x)函数通过传递一个x拷贝创建了v,那么v的改变并不能更改原始的x。要想v的更改能作用到x,那就必须传递x的地址v = reflect.ValueOf(&x)。
通过Type()我们看到v现在的类型是*float64并且仍然是不可设置的。
要想让其可设置我们需要使用Elem()函数,这间接的使用指针:v = v.Elem()
现在v.CanSet()返回true并且v.SetFloat(3.1415)设置成功了!
示例 reflect2.go:
// reflect2.go package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 v := reflect.ValueOf(x) // setting a value: // v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable value fmt.Println("settability of v:", v.CanSet()) v = reflect.ValueOf(&x) // Note: take the address of x. fmt.Println("type of v:", v.Type()) fmt.Println("settability of v:", v.CanSet()) v = v.Elem() fmt.Println("The Elem of v is: ", v) fmt.Println("settability of v:", v.CanSet()) v.SetFloat(3.1415) // this works! fmt.Println(v.Interface()) fmt.Println(v) } /* Output: settability of v: false type of v: *float64 settability of v: false The Elem of v is: settability of v: true 3.1415 */
反射中有些内容是需要用地址去改变它的状态的。
3)反射结构
有些时候需要反射一个结构类型。NumField()方法返回结构内的字段数量;可以通过一个for循环通过索引取得每个字段的值Field(i)。
我们同样能够调用签名在结构上的方法,例如,使用索引n来调用:Method(n).Call(nil)
示例 reflect_struct.go:
// reflect.go package main import ( "fmt" "reflect" ) type NotknownType struct { s1, s2, s3 string } func (n NotknownType) String() string { return n.s1 + " - " + n.s2 + " - " + n.s3 } // variable to investigate: var secret interface{} = NotknownType{"Ada", "Go", "Oberon"} func main() { value := reflect.ValueOf(secret) // typ := reflect.TypeOf(secret) // main.NotknownType // alternative: //typ := value.Type() // main.NotknownType fmt.Println(typ) knd := value.Kind() // struct fmt.Println(knd) // iterate through the fields of the struct: for i := 0; i < value.NumField(); i++ { fmt.Printf("Field %d: %v\n", i, value.Field(i)) // error: panic: reflect.Value.SetString using value obtained using unexported field //value.Field(i).SetString("C#") } // call the first method, which is String(): results := value.Method(0).Call(nil) fmt.Println(results) // [Ada - Go - Oberon] } /* Output: main.NotknownType struct Field 0: Ada Field 1: Go Field 2: Oberon [Ada - Go - Oberon] */
但是如果尝试更改一个值,会得到一个错:
panic: reflect.Value.SetString using value obtained using unexported field
这是因为结构中只有被导出字段(首字母大写)才是可设置的;来看下面的例子:
示例 reflect_struct2.go:
9、实现多态的方法// reflect_struct2.go package main import ( "fmt" "reflect" ) type T struct { A int B string } func main() { t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) } s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) } /* Output: 0: A int = 23 1: B string = skidoo t is now {77 Sunset Strip} */
我们先看一个示例:
type IMessage interface { Print() } type BaseMessage struct { //IMessage 没有必要embedding这个interface,因为只是按照契约实现接口,但是并没有利用接口的数据和功能 //一般说来直接实现接口的类都没有必要embedding接口 msg string } func (message *BaseMessage) Print() { fmt.Println("baseMessage:", message.msg) } type SubMessage struct { BaseMessage //因为要使用BaseMessage的数据,所以必须embedding } func (message *SubMessage) Print() { fmt.Println("subMessage:", message.msg) } func interface_use(i IMessage) { i.Print() } func main() { baseMessage := new(BaseMessage) baseMessage.msg = "a" interface_use(baseMessage) // 输出:baseMessage:a SubMessage := new(SubMessage) SubMessage.msg = "b" interface_use(SubMessage) // 输出:subMessage:b }
对于上面代码,看起来可能不难,但是用到的知识点却非常多,我们解读一下:
-
实现接口:结构体BaseMessage,实现了IMessage接口的所有方法;
-
匿名字段嵌入 + 方法覆盖:结构体SubMessage嵌入了BaseMessage,也实现了自己的Print(),所以实际使用时,SubMessage.Print()会覆盖BaseMessage.Print();
-
接口转换:BaseMessage和SubMessage可以转换为IMessage接口,代码里面是隐式转换;
-
动态类型:BaseMessage和SubMessage都实现了IMessage接口,所以通过隐式转换后,IMessage接口的动态类型和动态值会相应改变,当动态类型为BaseMessage时,执行i.Print(),执行的是BaseMessage.Print();当动态类型为SubMessage,执行i.Print(),执行的是SubMessage.Print()。
我觉得上面这段代码,其实是Go非常常用的一种方式,会用到Go的常用设计模式中,我们可以通过下一节的示例,再巩固一下。
1)实战:设计模式之组合模式
// Context 上下文 type Context struct{} // Component 组件接口 type Component interface { Mount(c Component, components ...Component) error // 添加一个子组件 Remove(c Component) error // 移除一个子组件 Do(ctx *Context) error // 执行组件&子组件 } // BaseComponent 基础组件 // 实现Add:添加一个子组件 // 实现Remove:移除一个子组件 type BaseComponent struct { ChildComponents []Component // 子组件列表 } // Mount 挂载一个子组件 func (bc *BaseComponent) Mount(c Component, components ...Component) (err error) { bc.ChildComponents = append(bc.ChildComponents, c) if len(components) == 0 { return } bc.ChildComponents = append(bc.ChildComponents, components...) return } // Remove 移除一个子组件 func (bc *BaseComponent) Remove(c Component) (err error) { if len(bc.ChildComponents) == 0 { return } for k, childComponent := range bc.ChildComponents { if c == childComponent { fmt.Println(runFuncName(), "移除:", reflect.TypeOf(childComponent)) bc.ChildComponents = append(bc.ChildComponents[:k], bc.ChildComponents[k+1:]...) } } return } // Do 执行组件&子组件 func (bc *BaseComponent) Do(ctx *Context) (err error) { // do nothing return } // ChildsDo 执行子组件 func (bc *BaseComponent) ChildsDo(ctx *Context) (err error) { // 执行子组件 for _, childComponent := range bc.ChildComponents { if err = childComponent.Do(ctx); err != nil { return err } } return } // CheckoutPageComponent 订单结算页面组件 type CheckoutPageComponent struct { // 合成复用基础组件 BaseComponent } // Do 执行组件&子组件 func (bc *CheckoutPageComponent) Do(ctx *Context) (err error) { // 当前组件的业务逻辑写这 fmt.Println(runFuncName(), "订单结算页面组件...") // 执行子组件 bc.ChildsDo(ctx) // 当前组件的业务逻辑写这 return } // AddressComponent 地址组件 type AddressComponent struct { BaseComponent } func (bc *AddressComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "地址组件...") bc.ChildsDo(ctx) return } // StoreComponent 店铺组件 type StoreComponent struct { BaseComponent } func (bc *StoreComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "店铺组件...") bc.ChildsDo(ctx) return } // SkuComponent 商品组件 type SkuComponent struct { BaseComponent } func (bc *SkuComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "商品组件...") bc.ChildsDo(ctx) return } // PromotionComponent 优惠信息组件 type PromotionComponent struct { BaseComponent } func (bc *PromotionComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "优惠信息组件...") bc.ChildsDo(ctx) return } // ExpressComponent 物流组件 type ExpressComponent struct { BaseComponent } func (bc *ExpressComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "物流组件...") bc.ChildsDo(ctx) return } // AftersaleComponent 售后组件 type AftersaleComponent struct { BaseComponent } func (bc *AftersaleComponent) Do(ctx *Context) (err error) { fmt.Println(runFuncName(), "售后组件...") bc.ChildsDo(ctx) return } func main() { // 初始化订单结算页面 这个大组件 checkoutPage := &CheckoutPageComponent{} // 挂载子组件 storeComponent := &StoreComponent{} skuComponent := &SkuComponent{} skuComponent.Mount( &PromotionComponent{}, &AftersaleComponent{}, ) storeComponent.Mount( skuComponent, &ExpressComponent{}, ) // 挂载组件 checkoutPage.Mount( &AddressComponent{}, storeComponent, ) // 开始构建页面组件数据 checkoutPage.Do(&Context{}) }
这里其实不是为了去讲设计模式,主要是希望能借鉴这个示例,来巩固上面的知识,里面的代码细节,我就不再给大家剖析了,等后面有时间,我再把这个示例的前因后果,整体再讲一下。
2)实战:设计模式之工厂 + 策略模式
type Pool interface { Get() (io.Closer, error) Put(obj io.Closer) Close() error } // 工厂方法 func NewPool(type, name string, size int, newFunc func() (io.Closer, error)) Pool { if type == "chanPool" { return NewChanPool(name, size, newFunc) } else { return NewRingBufferPool(name, size, newFunc) } } // type chanPool struct { name string size int idle int32 max int32 ch chan io.Closer new func() (io.Closer, error) } func NewChanPool(name string, size int, newFunc func() (io.Closer, error)) Pool { return &chanPool{ name: name, size: size, ch: make(chan io.Closer, size), new: newFunc, } } // 假如实现了Pool接口,实现方法省略。。。 type ringBufferPool struct { sync.Once err error closed int32 name string rb *RingBuffer new func() (io.Closer, error) } func NewRingBufferPool(name string, size int, newFunc func() (io.Closer, error)) Pool { return &ringBufferPool{ err: errors.New("failed get object from ring buffer pool " + name), name: name, rb: NewRingBuffer(int32(size)), new: newFunc, } } // 实现了Pool接口,实现方法省略。。。 func main() { var pool pool := NewPool("chanPool", "testpool", 10, nil) pool.Close() // 调用的是chanPool的Close方法 pool := NewPool("ringBufferPool", "testpool", 10, nil) pool.Close() // 调用的是ringBufferPool的Close方法 }
这个示例,就当给大家额外学习,本来是不打算写在这里面的。实例化了2个结构体,分别为chanPool和ringBufferPoo,隐式转换为接口pool后,可以通过pool中的方法,动态调用实例化对象的方法。
总结:
接口这块知识,其实在我没有完全梳理完之前,总感觉有些知识点比较模糊,当碰到比较复杂的代码,就有种没有摸透的感觉,现在将它们全部梳理完后,感觉里面的语法就清晰了很多。
对于接口实现,如果大家对方法集不是特别清楚,这里其实是很容易入坑的,因为是否实现了该接口,编译器是根据方法集来判断的。然后就是接口组合,之前知道接口体有匿名字段,可以进行组合,这里又搞个接口组合,就有点迷糊了,其实两者有异曲同工,重要的区分就是接口组合是没有屏蔽的概念,如果定义相同的接口方法,编译器会直接报错的。
对于动态类型,这个可能是接口中稍微比较复杂的地方,我们要弄清楚的是,接口变量的动态值、动态类型都代表了什么,这些其实都是正确使用接口变量的基础。最后通过“实现多态的方法”和“设计模式之组合模式”两小节,将结构体、方法、匿名字段、动态类型、方法屏蔽、隐式转换等知识全部串起来,这些知识虽然看起来比较零散(很多书籍讲述这些知识时,也都是零散的去讲,有的甚至完全没有去讲,所以一直不能将这些知识点系统的串起来,这个也是我想写这个手册的初衷之一),但是他们直接其实有着千世万缕的联系。
最后,对于结构体内嵌结构体,结构体内嵌接口,然后一些常用专有名词的理解和设计模式的运用,我画个大图,大家可以一目了然。