Go 语言到底适合干什么?

【Go 语言到底适合干什么?】Go语言开发团队花了很长时间来解决当今软件开发人员面对的问题 。 开发人员在为项目选择语言时 , 不得不在快速开发和性能之间做出选择 。 C和C++这类语言提供了很快的执行速度 , 而Ruby和Python这类语言则擅长快速开发 。 Go语言在这两者间架起了桥梁 , 不仅提供了高性能的语言 , 同时也让开发更快速 。
在探索Go语言的过程中 , 读者会看到精心设计的特性以及简洁的语法 。 作为一门语言 , Go不仅定义了能做什么 , 还定义了不能做什么 。 Go语言的语法简洁到只有几个关键字 , 便于记忆 。 Go语言的编译器速度非常快 , 有时甚至会让人感觉不到在编译 。 所以 , Go开发者能显著减少等待项目构建的时间 。 因为Go语言内置并发机制 , 所以不用被迫使用特定的线程库 , 就能让软件扩展 , 使用更多的资源 。 Go语言的类型系统简单且高效 , 不需要为面向对象开发付出额外的心智 , 让开发者能专注于代码复用 。 Go语言还自带垃圾回收器 , 不需要用户自己管理内存 。 让我们快速浏览一下这些关键特性 。
1.1.1开发速度编译一个大型的C或者C++项目所花费的时间甚至比去喝杯咖啡的时间还长 。 图1-1是XKCD中的一幅漫画 , 描述了在办公室里开小差的经典借口 。
Go语言使用了更加智能的编译器 , 并简化了解决依赖的算法 , 最终提供了更快的编译速度 。 编译Go程序时 , 编译器只会关注那些直接被引用的库 , 而不是像Java、C和C++那样 , 要遍历依赖链中所有依赖的库 。 因此 , 很多Go程序可以在1秒内编译完 。 在现代硬件上 , 编译整个Go语言的源码树只需要20秒 。
因为没有从编译代码到执行代码的中间过程 , 用动态语言编写应用程序可以快速看到输出 。 代价是 , 动态语言不提供静态语言提供的类型安全特性 , 不得不经常用大量的测试套件来避免在运行的时候出现类型错误这类bug 。
想象一下 , 使用类似JavaScript这种动态语言开发一个大型应用程序 , 有一个函数期望接收一个叫作ID的字段 。 这个参数应该是整数 , 是字符串 , 还是一个UUID?要想知道答案 , 只能去看源代码 。 可以尝试使用一个数字或者字符串来执行这个函数 , 看看会发生什么 。 在Go语言里 , 完全不用为这件事情操心 , 因为编译器就能帮用户捕获这种类型错误 。
1.1.2并发作为程序员 , 要开发出能充分利用硬件资源的应用程序是一件很难的事情 。 现代计算机都拥有多个核 , 但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源 。 这些语言需要写大量的线程同步代码来利用多个核 , 很容易导致错误 。
Go语言对并发的支持是这门语言最重要的特性之一 。 goroutine很像线程 , 但是它占用的内存远少于线程 , 使用它需要的代码更少 。 通道(channel)是一种内置的数据结构 , 可以让用户在不同的goroutine之间同步发送具有类型的消息 。 这让编程模型更倾向于在goroutine之间发送消息 , 而不是让多个goroutine争夺同一个数据的使用权 。 让我们看看这些特性的细节 。
1.goroutinegoroutine是可以与其他goroutine并行执行的函数 , 同时也会与主程序(程序的入口)并行执行 。 在其他编程语言中 , 你需要用线程来完成同样的事情 , 而在Go语言中会使用同一个线程来执行多个goroutine 。 例如 , 用户在写一个Web服务器 , 希望同时处理不同的Web请求 , 如果使用C或者Java , 不得不写大量的额外代码来使用线程 。 在Go语言中 , net/http库直接使用了内置的goroutine 。 每个接收到的请求都自动在其自己的goroutine里处理 。 goroutine使用的内存比线程更少 , Go语言运行时会自动在配置的一组逻辑处理器上调度执行goroutine 。 每个逻辑处理器绑定到一个操作系统线程上(见图1-2) 。 这让用户的应用程序执行效率更高 , 而开发工作量显著减少 。
如果想在执行一段代码的同时 , 并行去做另外一些事情 , goroutine是很好的选择 。 下面是一个简单的例子:
funclog(msgstring){...这里是一些记录日志的代码}//代码里有些地方检测到了错误golog("发生了可怕的事情")关键字go是唯一需要去编写的代码 , 调度log函数作为独立的goroutine去运行 , 以便与其他goroutine并行执行 。 这意味着应用程序的其余部分会与记录日志并行执行 , 通常这种并行能让最终用户觉得性能更好 。 就像之前说的 , goroutine占用的资源更少 , 所以常常能启动成千上万个goroutine 。 我们会在第6章更加深入地探讨goroutine和并发 。
2.通道通道是一种数据结构 , 可以让goroutine之间进行安全的数据通信 。 通道可以帮用户避免其他语言里常见的共享内存访问的问题 。
并发的最难的部分就是要确保其他并发运行的进程、线程或goroutine不会意外修改用户的数据 。 当不同的线程在没有同步保护的情况下修改同一个数据时 , 总会发生灾难 。 在其他语言中 , 如果使用全局变量或者共享内存 , 必须使用复杂的锁规则来防止对同一个变量的不同步修改 。
为了解决这个问题 , 通道提供了一种新模式 , 从而保证并发修改时的数据安全 。 通道a
图1-3中有3个goroutine , 还有2个不带缓存的通道 。 第一个goroutine通过通道把数据传给已经在等待的第二个goroutine 。 在两个goroutine间传输数据是同步的 , 一旦传输完成 , 两个goroutine都会知道数据已经完成传输 。 当第二个goroutine利用这个数据完成其任务后 , 将这个数据传给第三个正在等待的goroutine 。 这次传输依旧是同步的 , 两个goroutine都会确认数据传输完成 。 这种在goroutine之间安全传输数据的方法不需要任何锁或者同步机制 。
需要强调的是 , 通道并不提供跨goroutine的数据访问保护机制 。 如果通过通道传输数据的一份副本 , 那么每个goroutine都持有一份副本 , 各自对自己的副本做修改是安全的 。 当传输的是指向数据的指针时 , 如果读和写是由不同的goroutine完成的 , 每个goroutine依旧需要额外的同步动作 。
1.1.3Go语言的类型系统Go语言提供了灵活的、无继承的类型系统 , 无需降低运行性能就能最大程度上复用代码 。 这个类型系统依然支持面向对象开发 , 但避免了传统面向对象的问题 。 如果你曾经在复杂的Java和C++程序上花数周时间考虑如何抽象类和接口 , 你就能意识到Go语言的类型系统有多么简单 。 Go开发者使用组合(composition)设计模式 , 只需简单地将一个类型嵌入到另一个类型 , 就能复用所有的功能 。 其他语言也能使用组合 , 但是不得不和继承绑在一起使用 , 结果使整个用法非常复杂 , 很难使用 。 在Go语言中 , 一个类型由其他更微小的类型组合而成 , 避免了传统的基于继承的模型 。
另外 , Go语言还具有独特的接口实现机制 , 允许用户对行为进行建模 , 而不是对类型进行建模 。 在Go语言中 , 不需要声明某个类型实现了某个接口 , 编译器会判断一个类型的实例是否符合正在使用的接口 。 Go标准库里的很多接口都非常简单 , 只开放几个函数 。 从实践上讲 , 尤其对那些使用类似Java的面向对象语言的人来说 , 需要一些时间才能习惯这个特性 。
1.类型简单Go语言不仅有类似int和string这样的内置类型 , 还支持用户定义的类型 。 在Go语言中 , 用户定义的类型通常包含一组带类型的字段 , 用于存储数据 。 Go语言的用户定义的类型看起来和C语言的结构很像 , 用起来也很相似 。 不过Go语言的类型可以声明操作该类型数据的方法 。 传统语言使用继承来扩展结构——Client继承自User , User继承自Entity , Go语言与此不同 , Go开发者构建更小的类型——Customer和Admin , 然后把这些小类型组合成更大的类型 。 图1-4展示了继承和组合之间的不同 。
2.Go接口对一组行为建模接口用于描述类型的行为 。 如果一个类型的实例实现了一个接口 , 意味着这个实例可以执行一组特定的行为 。 你甚至不需要去声明这个实例实现某个接口 , 只需要实现这组行为就好 。 其他的语言把这个特性叫作鸭子类型——如果它叫起来像鸭子 , 那它就可能是只鸭子 。 Go语言的接口也是这么做的 。 在Go语言中 , 如果一个类型实现了一个接口的所有方法 , 那么这个类型的实例就可以存储在这个接口类型的实例中 , 不需要额外声明 。
在类似Java这种严格的面向对象语言中 , 所有的设计都围绕接口展开 。 在编码前 , 用户经常不得不思考一个庞大的继承链 。 下面是一个Java接口的例子:
interfaceUser{publicvoidlogin();publicvoidlogout();}在Java中要实现这个接口 , 要求用户的类必须满足User接口里的所有约束 , 并且显式声明这个类实现了这个接口 。 而Go语言的接口一般只会描述一个单一的动作 。 在Go语言中 , 最常使用的接口之一是io.Reader 。 这个接口提供了一个简单的方法 , 用来声明一个类型有数据可以读取 。 标准库内的其他函数都能理解这个接口 。 这个接口的定义如下:
typeReaderinterface{Read(p[]byte)(nint,errerror)}为了实现io.Reader这个接口 , 你只需要实现一个Read方法 , 这个方法接受一个byte切片 , 返回一个整数和可能出现的错误 。
这和传统的面向对象编程语言的接口系统有本质的区别 。 Go语言的接口更小 , 只倾向于定义一个单一的动作 。 实际使用中 , 这更有利于使用组合来复用代码 。 用户几乎可以给所有包含数据的类型实现io.Reader接口 , 然后把这个类型的实例传给任意一个知道如何读取io.Reader的Go函数 。
Go语言的整个网络库都使用了io.Reader接口 , 这样可以将程序的功能和不同网络的实现分离 。 这样的接口用起来有趣、优雅且自由 。 文件、缓冲区、套接字以及其他的数据源都实现了io.Reader接口 。 使用同一个接口 , 可以高效地操作数据 , 而不用考虑到底数据来自哪里 。
1.1.4内存管理不当的内存管理会导致程序崩溃或者内存泄漏 , 甚至让整个操作系统崩溃 。 Go语言拥有现代化的垃圾回收机制 , 能帮你解决这个难题 。 在其他系统语言(如C或者C++)中 , 使用内存前要先分配这段内存 , 而且使用完毕后要将其释放掉 。 哪怕只做错了一件事 , 都可能导致程序崩溃或者内存泄漏 。 可惜 , 追踪内存是否还被使用本身就是十分艰难的事情 , 而要想支持多线程和高并发 , 更是让这件事难上加难 。 虽然Go语言的垃圾回收会有一些额外的开销 , 但是编程时 , 能显著降低开发难度 。 Go语言把无趣的内存管理交给专业的编译器去做 , 而让程序员专注于更有趣的事情 。
本文摘自《Go语言实战》
本书是写给有其他编程语言基础且有一定开发经验的、想学Go语言的中级开发者的 。 对于刚开始要学习Go语言和想要深入了解Go语言内部实现的人来说 , 本书都是最佳的选择 。


    推荐阅读