关于GO语言,这篇文章讲的很明白
日期: 2020-10-13 分类: 跨站数据 306次阅读
摘要:本文从Go的语法,类型系统,编码风格,语言工具,编码工具和使用案例等几方面对Go语言进行了学习和探讨。
Go语言发布之后,很多公司特别是云厂商也开始用Go语言重构产品的基础架构,而且很多企业都是直接采用Go语言进行开发,最近热火朝天的Docker就是采用Go语言进行开发的。本文我们一起来探讨和学习一下Go语言的技术特点。先来看个例子:
package main
import (
"fmt"
"time"
)
// 要在goroutine中运行的函数。done通道将被用来通知工作已经完成。
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
// 通知完成。
done <- true
}
func main() {
// 创建一个通道
done := make(chan bool, 1)
go worker(done)
// 等待done变为true
<-done
}
上例中是一个在Go语言中使用goroutine和通道的例子。 其中:
go 关键字是用来启动一个goroutine
done <- true, 向通道传值
<-done, 读取通道值
一、【概述】
Go是由RobertGriesemer、RobPike和KenThompson在Google设计的一种静态类型化的、须编译后才能运行的编程语言。
Go在语法上类似于C语言,但它具有C语言没有的优势,如内存安全、垃圾回收、结构化的类型和CSP风格的并发性。
它的域名是http://golang.org,所以通常被称为"Golang",但正确的名称是Go。
二、【GO的设计思想】
Go的设计受C语言的影响,但更加简单和安全。该语言包括如下特点:
- 采用动态语言中比较常见的语法和环境模式:
- 可选的简明变量声明和通过类型推理进行初始化(如果使用x := 0而不是int x= 0;或var x= 0;)。
- 快速编译。
- 远程包管理(go get)和在线包文档。
- 针对特定问题的独特方法:
- 内置的并发基元:轻量级处理机制(goroutines)、通道和select语句。
- 用接口系统代替虚拟继承,用类型嵌入代替非虚拟继承。
- 默认情况下,由一个工具链生成静态链接的原生二进制文件,没有外部依赖关系。
- 希望保持语言规范足够简单,程序员容易掌握。
2.1 简洁的语法
Go的语法包含C语言中保持代码简洁性和可读性的语法特点。
2.1.1 变量声明
引入了一个联合声明/初始化操作符,允许程序员写出i := 3或s :="Hello, world!",而不需要指定使用的变量类型。
这与C语言中的int i= 3; 和 const char *s = "Hello, world!";形成鲜明对比。
2.1.2 分号隐含
分号仍然是终止语句,但在行结束时是隐含的。
2.1.3 返回多值
在Go中,一个函数方法可以返回多个值,返回一个结果和错误err组合对是向调用者提示错误的常规方式。
2.1.4 范围表达式
Go的范围表达式允许在数组、动态数组、字符串、字典和通道上进行简洁的迭代,在C语言中,有三种循环来实现这个功能。
2.2 类型系统
2.2.1 内置的类型
Go有许多内置的类型,包括数字类型(byte、int64、float32等)、booleans和字符串(string)。
字符串是不可更改的。
内置的运算符和关键字(而不是函数)提供了串联、比较和UTF-8编码/解码。
2.2.2 结构类型
记录类型可以用struct关键字定义。
2.2.3 数组类型
对于每个类型T和每个非负整数常数n,都有一个数组类型,表示为[n]T,因此,不同长度的数组有不同的类型。
动态数组可以作为"Slice"使用,如对于某类型T,表示为[]T。这些数组有一个长度和一个容量,容量规定了何时需要分配新的内存来扩展数组。若干个Slice可以共享它们的底层内存。
2.2.4 指针
所有类型都可以定义指针, T类型的指针可定义为*T。地址抽取和隐式访问使用&和*操作符,这跟C语言一样,或者隐式的通过方法调用或属性访问使用。
除了标准库中的特殊的unsafe.Pointer类型,一般指针没有指针运算。
2.2.5 映射类型
对于一个组合对类型K、V,类型map[K]V是将类型K键映射到类型V值的哈希表的类型。
2.2.6 通道类型
chan T是一个通道,允许在并发的Go进程之间发送T类型的值。
2.2.7 显式类型
除了对接口的支持外,Go的类型系统是显示的:类型关键字可以用来定义一个新的命名类型,它与其他具有相同布局的命名类型(对于结构体来说,相同的成员按相同的顺序排列)不同。类型之间的一些转换(如各种整数类型之间的转换)是预先定义好的,添加一个新的类型可以定义额外的转换,但命名类型之间的转换必须始终显式调用。例如,类型关键字可以用来定义IPv4地址的类型,基于32位无符号整数:
type ipv4addr uint32
通过这个类型定义,ipv4addr(x)将uint32值x解释为IP地址。如果简单地将x分配给类型为ipv4addr的变量将会是一个类型错误。
常量表达式既可以是类型化的,也可以是 "非类型化的";如果它们所代表的值通过了编译时的检查,那么当它们被分配给一个类型化的变量时,就会被赋予一个类型。
2.2.8 函数类型
函数类型由func关键字表示;它们取0个或更多的参数并返回0个或更多的值,这些值都是要声明类型的。
参数和返回值决定了一个函数的类型;比如,func(string, int32)(int, error)就是输入一个字符串和一个32位有符号的整数,并返回一个有符号的整数和一个错误(内置接口类型)的值的函数类型。
2.2.9 类型上的方法扩展
任何命名的类型都有一个与之相关联的方法集合。上面的IP地址例子可以用一个检查其值是否为已知标准的方法来扩展:
// ZeroBroadcast报告addr是否为255.255.255.255.255。
func (addr ipv4addr) ZeroBroadcast() bool {
return addr == 0xFFFFFFFF
}
以上的函数在ipv4addr上增加了一个方法,但这个方法在uint32上没有。
2.2.10 接口系统
Go提供了两个功能来取代类继承。
首先是嵌入方法,可以看成是一种自动化的构成形式或委托代理。
第二种是接口,它提供了运行时的多态性。
接口是一类型,它在Go的类型系统中提供了一种有限的结构类型化形式。
一个接口类型的对象同时也有另一种类型的定义对应,这点就像C++对象同时具有基类和派生类的特征一样。
Go接口是在Smalltalk编程语言的协议基础上设计的。
在描述Go接口时使用了鸭式填充这个术语。
虽然鸭式填充这个术语没有精确的定义,它通常是说这些对象的类型一致性没有被静态检查。
由于Go接口的一致性是由Go编译器静态地检查的,所以Go的作者们更喜欢使用结构类型化这个词。
接口类型的定义按名称和类型列出了所需的方法。任何存在与接口类型I的所需方法匹配的函数的T类型的对象也是类型I的对象。类型T的定义不需要也不能识别类型I。例如,如果Shape、Square和Circle被定义为:
import "math"
type Shape interface {
Area() float64
}
type Square struct { // 注:没有 "实现 "声明
side float64
}
func (sq Square) Area() float64 { return sq.side * sq.side }
type Circle struct { // 这里也没有 "实现 "声明
radius float64
}
func (c Circle) Area() float64 { return math.Pi * math.Pow(c.radius, 2) }
一个正方形和一个圆都隐含着一个形状(Shape)类型,并且可以被分配给一个形状(Shape)类型的变量。
Go的接口系统使用了了结构类型。接口也可以嵌入其他接口,其效果是创建一个组合接口,而这个组合接口正是由实现嵌入接口的类型和新定义的接口所增加的方法来满足的。
Go标准库在多个地方使用接口来提供通用性,这包括基于Reader和Writer概念的输入输出系统。
除了通过接口调用方法,Go还允许通过运行时类型检查将接口值转换为其他类型。这就是类型断言和类型切换。
空接口{}是一个重要的基本情况,因为它可以引用任何类型的选项。它类似于Java或C#中的Object类,可以满足任何类型,包括像int这样的内置类型。
使用空接口的代码不能简单地在被引用的对象上调用方法或内置操作符,但它可以存储interface{}值,通过类型断言或类型切换尝试将其转换为更有用的类型,或者用Go的reflect包来检查它。
因为 interface{} 可以引用任何值,所以它是一种摆脱静态类型化限制的有效方式,就像C 语言中的 void*,但在运行时会有额外的类型检查。
接口值是使用指向数据的指针和第二个指向运行时类型信息的指针来实现的。与Go中其他一些使用指针实现的类型一样,如果未初始化,接口值是零。
2.3 程序包系统
在Go的包系统中,每个包都有一个路径(如"compress/bzip2 "或"golang.org/x/net/html")和一个名称(如bzip2或html)。
对其他包的定义的引用必须始终以其他包的名称作为前缀,并且只有其他包的大写的名称才能被访问:io.Reader是公开的,但bzip2.reader不是。
go get命令可以检索存储在远程资源库中的包,鼓励开发者在开发包时,在与源资源库相对应的基础路径
(如http://example.com/user_name/package_name)内开发程序包,从而减少将来在标准库或其他外部库中名称碰撞的可能性。
有人提议Go引入一个合适的包管理解决方案,类似于CPANfor Perl或Rust的Cargo系统或Node的npm系统。
2.4 并发:goroutines和通道
2.4.1 【CSP并发模式】
在计算机科学中,通信顺序过程(communicating sequential processes,CSP)是一种描述并发系统中交互模式的正式语言,它是并发数学理论家族中的一个成员,被称为过程算法(process algebras),或者说过程计算(process calculate),是基于消息的通道传递的数学理论。
CSP在设计Oceam编程语言时起了很大的影响,同时也影响了Limbo、RaftLib、Go、Crystal和Clojure的core.async等编程语言的设计。
CSP最早是由TonyHoare在1978年的一篇论文中描述的,后来有了很大的发展。
CSP作为一种工具被实际应用于工业上,用于指定和验证各种不同系统的并发功能,如T9000Transputer以及安全的电子商务系统。
CSP本身的理论目前也仍然是被积极研究的对象,包括增加其实际适用范围的工作,如增加可分析的系统规模。
Go语言有内置的机制和库支持来编写并发程序。并发不仅指的是CPU的并行性,还指的是异步性处理:让相对慢的操作,如数据库或网络读取等操作在做其他工作的同时运行,这在基于事件的服务器中很常见。
主要的并发构造是goroutine,这是一种轻量级处理类型。一个以go关键字为前缀的函数调用会在一个新的goroutine中启动这个函数。
语言规范并没有指定如何实现goroutine,但目前的实现将Go进程的goroutine复用到一个较小的操作系统线程集上,类似于Erlang中的调度。
虽然一个标准的库包具有大多数经典的并发控制结构(mutex锁等),但Go并发程序更偏重于通道,它提供了goroutines之间的消息传功能。
可选的缓冲区以FIFO顺序存储消息,允许发送的goroutines在收到消息之前继续进行。
通道是类型化的,所以chan T类型的通道只能用于传输T类型的消息。
特殊语法约定用于对它们进行操作;<-ch是一个表达式,它使执行中的goroutine在通道ch上阻塞,直到有一个值进来,而ch<- x则是发送值x(可能阻塞直到另一个goroutine接收到这个值)。
内置的类似于开关的选择语句可以用来实现多通道上的非阻塞通信。Go有一个内存模型,描述了goroutine必须如何使用通道或其他操作来安全地共享数据。
通道的存在使Go有别于像Erlang这样的actor模型式的并发语言,在这种语言中,消息是直接面向actor(对应于goroutine)的。在Go中,可以通过在goroutine和通道之间保持一对一的对应关系来,Go语言也允许多个goroutine共享一个通道,或者一个goroutine在多个通道上发送和接收消息。
通过这些功能,人们可以构建像workerpools、流水线(比如说,在下载文件时,对文件进行解压缩和解析)、带超时的后台调用、对一组服务的"扇出"并行调用等并发构造。
通道也有一些超越进程间通信的常规概念的用途,比如作为一个并发安全的回收缓冲区列表,实现coroutines和实现迭代器。
Go的并发相关的结构约定(通道和替代通道输入)来自于TonyHoare的通信顺序进程模型。
不像以前的并发编程语言,如Occam或Limbo(Go的共同设计者RobPike曾在此基础上工作过的语言),Go没有提供任何内置的安全或可验证的并发概念。
虽然在Go中,上述的通信处理模型是推荐使用的,但不是唯一的:一个程序中的所有goroutines共享一个单一的地址空间。这意味着可突变对象和指针可以在goroutines之间共享。
2.5 并行编程的舒适度
有一项研究比较了一个不熟悉Go语言的老练程序员编写的程序的大小(以代码行数为单位)和速度,以及一个Go专家(来自Google开发团队)对这些程序的修正,对Chapel、Cilk和IntelTBB做了同样的研究。
研究发现,非专家倾向于用每个递归中的一条Go语句来写分解-解决算法,而专家则用每个处理器的一条Go语句来写分布式工作同步程序。Go专家的程序通常更快,但也更长。
2.6 条件竞赛安全问题
Goroutine对于如何访问共享数据没有限制,这使得条件竞赛成为可能的问题。
具体来说,除非程序通过通道或其他方式显式同步,否则多个goroutine共享读写一个内存区域可能会发生问题。
此外,Go的内部数据结构,如接口值、动态数组头、哈希表和字符串头等内部数据结构也不能幸免于条件竞赛,因此在多线程程序中,如果修改这些类型的共享实例没有同步,就会存在影响类型和内存安全的情况。
2.7 二进制生成
gc工具链中的链接器默认会创建静态链接的二进制文件,因此所有的Go二进制文件都包括Go运行所需要的内容。
2.8 舍弃的语言特征
Go故意省略了其他语言中常见的一些功能,包括继承、通用编程、断言、指针运算、隐式类型转换、无标记的联合和标记联合。
2.9 Go风格特点
Go作者在Go程序的风格方面付出了大量的努力:
- gofmt工具自动规范了代码的缩进、间距和其他表面级的细节。
- 与Go一起分发的工具和库推荐了一些标准的方法,比如API文档(godoc)、 测试(go test)、构建(go build)、包管理(go get)等等。
- Go的一些规则跟其他语言不同,例如禁止循环依赖、未使用的变量或导入、隐式类型转换等。
- 某些特性的省略(例如,函数编程的一些捷径,如map和Java风格的try/finally块)编程风格显式化,具体化,简单化。
- Go团队从第一天开始就发布了一个Go的语法使用集合,后来还收集了一些代码的评论,讲座和官方博客文章,来推广Go的风格和编码理念。
三、【Go的工具】
主要的Go发行版包括构建、测试和分析代码的工具。
go build,它只使用源文件中的信息来构建Go二进制文件,不使用单独的makefiles。
gotest,用于单元测试和微基准
go fmt,用于格式化代码
go get,用于检索和安装远程包。
go vet,静态分析器,查找代码中的潜在错误。
go run,构建和执行代码的快捷方式
godoc,用于显示文档或通过HTTP
gorename,用于以类型安全的方式重命名变量、函数等。
go generate,一个标准的调用代码生成器的方法。
它还包括分析和调试支持、运行时诊断(例如,跟踪垃圾收集暂停)和条件竞赛测试器。
第三方工具的生态系统增强了标准的发布系统,如:
gocode,它可以在许多文本编辑器中自动完成代码,
goimports(由Go团队成员提供),它可以根据需要自动添加/删除包导入,以及errcheck,它可以检测可能无意中被忽略的错误代码。
四、【编辑环境】
流行的Go代码工具:
GoLand:JetBrains公司的IDE。
VisualStudio Code
LiteIDE:一个"简单、开源、跨平台的GoIDE"
Vim:用户可以安装插件:
vim-go
五、【应用案例】
用Go编写的一些著名的开源应用包括:
Caddy,一个开源的HTTP/2web服务器,具有自动HTTPS功能。
CockroachDB,一个开源的、可生存的、强一致性、可扩展的SQL数据库。
Docker,一套用于部署Linux容器的工具。
Ethereum,以太币虚拟机区块链的Go-Ethereum实现。
Hugo,一个静态网站生成器
InfluxDB,一个专门用于处理高可用性和高性能要求的时间序列数据的开源数据库。
InterPlanetaryFile System,一个可内容寻址、点对点的超媒体协议。
Juju,由UbuntuLinux的包装商Canonical公司推出的服务协调工具。
Kubernetes容器管理系统
lnd,比特币闪电网络的实现。
Mattermost,一个团队聊天系统
NATSMessaging,是一个开源的消息传递系统,其核心设计原则是性能、可扩展性和易用性。
OpenShift,云计算服务平台
Snappy,一个由Canonical开发的UbuntuTouch软件包管理器。
Syncthing,一个开源的文件同步客户端/服务器应用程序。
Terraform,是HashiCorp公司的一款开源的多云基础设施配置工具。
其他使用Go的知名公司和网站包括:
Cacoo,使用Go和gRPC渲染用户仪表板页面和微服务。
Chango,程序化广告公司,在其实时竞价系统中使用Go。
CloudFoundry,平台即服务系统
Cloudflare,三角编码代理Railgun,分布式DNS服务,以及密码学、日志、流处理和访问SPDY网站的工具。
容器Linux(原CoreOS),是一个基于Linux的操作系统,使用Docker容器和rkt容器。
Couchbase、Couchbase服务器内的查询和索引服务。
Dropbox,将部分关键组件从Python迁移到了Go。
谷歌,许多项目,特别是下载服务器dl.google.com。
Heroku,Doozer,一个提供锁具服务的公司
HyperledgerFabric,一个开源的企业级分布式分类账项目。
MongoDB,管理MongoDB实例的工具。
Netflix的服务器架构的两个部分。
Nutanix,用于其企业云操作系统中的各种微服务。
Plug.dj,一个互动式在线社交音乐流媒体网站。
SendGrid是一家位于科罗拉多州博尔德市的事务性电子邮件发送和管理服务。
SoundCloud,"几十个系统"
Splice,其在线音乐协作平台的整个后端(API和解析器)。
ThoughtWorks,持续传递和即时信息的工具和应用(CoyIM)。
Twitch,他们基于IRC的聊天系统(从Python移植过来的)。
Uber,处理大量基于地理信息的查询。
六、【代码示例】
6.1 Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
6.2 并发
package main
import (
"fmt"
"time"
)
func readword(ch chan string) {
fmt.Println("Type a word, then hit Enter.")
var word string
fmt.Scanf("%s", &word)
ch <- word
}
func timeout(t chan bool) {
time.Sleep(5 * time.Second)
t <- false
}
func main() {
t := make(chan bool)
go timeout(t)
ch := make(chan string)
go readword(ch)
select {
case word := <-ch:
fmt.Println("Received", word)
case <-t:
fmt.Println("Timeout.")
}
}
6.3 代码测试
没有测试的代码是不完整的,因此我们需要看看代码测试部分的编写。
代码:
func ExtractUsername(email string) string {
at := strings.Index(email, "@")
return email[:at]
}
测试案例:
func TestExtractUsername(t *testing.T) {
type args struct {
email string
}
tests := []struct {
name string
args args
want string
}{
{"withoutDot", args{email: "r@google.com"}, "r"},
{"withDot", args{email: "jonh.smith@example.com"}, "jonh.smith"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ExtractUsername(tt.args.email); got != tt.want {
t.Errorf("ExtractUsername() = %v, want %v", got, tt.want)
}
})
}
}
6.4 创建后端服务
接下来我写一个例子创建REST API后端服务:
我们的服务提供如下的API:
###
GET http://localhost:10000/
###
GET http://localhost:10000/all
###
GET http://localhost:10000 除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
标签:技术交流 程序员
精华推荐