CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go项目架构设计 说明
  • 1. 用Go构建大型项目
  • 2. 代码的封装
  • 3 设计模式
  • 4. 应用程序扩展
  • 5. 分布式架构
  • 6 消息传递
  • 7. 构建API
  • 8 数据建模
  • 9 反脆弱系统
  • 10 案例研究:旅游网站
  • 11. 部署规划
  • 12 应用程序迁移
    • 迁移的原因
      • Python
      • Java语言
    • 迁移策略
      • 阶段1 - 学习Go语言
      • 阶段2 - 试点服务迁移
      • 阶段3 - 定义包
      • 阶段4 - 移植主程序
      • 阶段5 - 移植包
      • 阶段6 - 优化计算
    • 组建团队
    • 总结
目录

12 应用程序迁移

# 12 应用程序迁移

Go语言是一门相对较新的编程语言,但它凭借在开发者中极高的喜爱度和使用率迅速声名远扬。人们热衷于用它来编写代码!

以下是最受喜爱的编程语言排名:

img

最受期待的编程语言排名如下图所示:

img

参考:https://insights.stackoverflow.com/survey/2018

鉴于Go语言引发的热潮,许多编程工作都涉及对现有代码进行迁移。本章将探讨从其他语言(如Python和Java)迁移到Go语言的一些驱动因素。我们还将为有这些语言背景、打算学习Go语言的开发者指出一些需要注意的问题。最后,我们还会介绍一种编排迁移的流程(包括处理泛型的策略)。

# 迁移的原因

在本节中,我们将探讨人们在使用各种语言时遇到的问题,以及迁移到Go语言是否能解决这些问题。

# Python

Python是一种解释型编程语言,以其表达能力和支持高速产品开发的能力而闻名。它的生态系统还包含强大的框架,比如Django,这使得构建Web应用程序变得极其容易且不易出错。许多研究(如以下参考文献中的研究)一致表明,Python的开发效率是Java的两倍多:

img

参考:http://www.connellybarnes.com/documents/language_productivity.pdf

话虽如此,Python也并非毫无缺点。以下列举了人们在使用Python进行开发时面临的一些挑战:

  • 性能:由于Python是一种解释型语言,计算性能通常比编译为本机代码的程序要慢得多,这是因为存在额外的间接层次。此外,由于全局解释器锁(GIL,Global Interpreter Lock)的存在,Python程序中存在大量线程序列化的情况。全局解释器锁是Python解释器内部的一个锁(互斥锁),由于解释器不是线程安全的,所以需要这个锁。如果没有这个锁,在解释器中执行的多个线程可能会导致严重的一致性问题。例如,两个线程可能同时增加同一个对象的引用计数,由于竞态条件,最终引用计数可能只增加了一次。为了避免这类问题,解释器内部的Python代码会通过获取全局解释器锁来进行序列化。这对多线程程序的性能产生了相当大的影响。下表展示了Python与Go语言的性能基准测试结果:

    基准测试 Go语言执行时间(秒) Python执行时间(秒)
    曼德布洛特(Mandelbrot) 5.48 279.68
    谱范数(spectral-norm) 3.94 193.86
    二叉树(binary-trees) 28.80 93.40
    n体问题(n-body) 21.37 882.00
    k-核苷酸(k-nucleotide) 12.77 79.79

​ 参考:https://benchmarksgame-team.pages.debian.net/benchmarksgame/faster/go-python3.html

虽然有第三方库(如gevent)声称可以实现并发,但编程模型较为复杂,并且它们依赖于对其他库的猴子补丁(monkey-patching)操作,这可能会导致意外行为。

  • 开发者导致的复杂性:Python是一种非常动态的语言,有时这种自由度会让开发者过度发挥,写出一些看似巧妙但实际上很难读懂、理解和维护的代码。仅通过阅读代码,你无法理解其含义;你需要运行多种场景才能了解代码的实际功能。下面是一个奇怪的例子,它将内部关键字True的值进行了修改:
>>>  True  =  False
>>>  if  True  ==  False:
...          print  "what  "
...
what
1
2
3
4
5
  • 动态类型:Python提供的编程自由度还导致了另一种复杂性:动态类型。考虑以下Python代码:
ages = { "Abe" : 10, "Bob"   : 11,  "Chris" : 12}
def ambiguos_age(name):
    to_ret = "not found"
    try :
        to_ret = ages[name]
    finally:
        return to_ret
print ambiguos_age("Abe")
print ambiguos_age("Des")
1
2
3
4
5
6
7
8
9

在这里,ambiguos_age()函数有时返回字符串,有时返回整数。这个例子可能看起来有些刻意,但在生产代码中,当函数嵌套较深时,很容易出现这种情况。这种行为可能会影响到外部接口(比如API的JSON响应)。现在,如果你有一个典型的Android应用(用强类型的Java编写),你可能会发现API在反序列化时有时会出现奇怪的错误。

对于Python程序员来说,转向Go语言也会有一些熟悉的地方。Go语言可以缓解Python存在的许多问题:

  • Go语言的编程模型充分体现了并发和并行性。Python程序员能立刻感受到在解决问题时,使用Go语言进行并发建模的自由度。
  • Go语言是强类型语言。ambiguos_age()这类代码中的错误会被编译器捕获!当我第一次将Python代码移植到Go语言时,我惊讶地发现生产环境中有那么多错误代码!虽然Go语言是静态类型语言,但它的类型推导功能使得代码更加简洁,这一点深受Python程序员喜爱。
  • 使用通道(channels)可以让许多多线程代码更易于阅读和维护。虽然将所有基于互斥锁加锁/解锁的代码重构为基于通道的管道和过滤器模型可能需要一些时间,但从长期维护的角度来看,这样做带来的好处远远超过了最初的移植成本。
  • 我(以及许多其他开发者)喜欢Python的一个优点是它有严格的格式化指南,特别是缩进规则。这使得程序布局保持一致,除其他优点外,还能加快代码审查速度。虽然Go语言在强制缩进方面没有那么严格,不过它要求代码块的起始括号与代码块起始行在同一行。go fmt工具可以方便地进行自动格式化(包括缩进),并且可以将其添加到Makefile中,以确保代码格式的一致性。

你可能会好奇,为什么Go语言有括号却没有分号?为什么不能把起始括号放在下一行呢?

Go语言使用括号来分组语句,对于使用过C家族中任何一种语言的程序员来说,这种语法都很熟悉。

然而,分号是给解析器用的,不是给人看的,我们尽可能地省去了分号。为了实现这个目标,Go语言借鉴了BCPL语言的技巧;在正式语法中,用于分隔语句的分号会在任何可能是语句结束的行尾由解析器自动插入,无需向前查找。在实践中,这种方式效果很好,但它也对括号的使用风格产生了影响。例如,函数的起始括号不能单独出现在一行上(https://golang.org/doc/faq#semicolons)。

在迁移过程中也会遇到一些问题:

  • 与大多数现代语言一样,Python将异常处理作为关键的语言结构。实际上,使用try-except结构进行尝试并捕获异常是Python的惯用写法。而Go语言的错误处理基本上是从函数中返回错误,并期望调用方处理该错误(这可能意味着将错误传递给调用方自己的调用者)。这导致错误处理代码非常冗长。此外,很容易编写忽略函数中错误的代码,这可能会导致错误行为甚至程序崩溃。
  • Go语言的包管理也存在一些问题。Python的虚拟环境概念(使用venv和requirements.txt需求文件)可以方便地管理依赖项。

在Go语言生态系统中,有一些即将推出的标准,如Dep和Govendor,旨在解决这个问题。正如我们之前看到的,另一种管理依赖项的简单方法是在GitHub仓库中使用vendor目录。

  • 有时Go语言代码比Python代码慢。是的,令人惊讶的是确实如此!几年前,Python的simplejson包在解码方面比encoding/json快约5倍,在编码方面比encoding/json快约2 - 3倍。这是因为Python代码实际上使用了C扩展。

JSON编码/解码在Go语言中已经变得更快了,并且有一个问题追踪(https://github.com/golang/go/issues/5683)。也有许多第三方JSON解析器声称速度要快得多(例如https://github.com/valyala/fastjson)。

  • Python程序员最初可能会感到恼火,因为编写能正常运行的Go代码需要更长时间(需要处理所有编译错误,包括未使用的导入,许多开发者认为将其作为错误有些过头了)。但这只是最初的感受,他们很快就会明白拥有更高质量代码所带来的长期好处。也有一些工具,如goimports(go fmt的分支),你可以将其集成到编辑器中,在保存文件时运行。

Go语言将未使用的导入标记为错误,以提高构建速度和程序清晰度。你可以在https://golang.org/doc/faq#unused_variables_and_imports上了解更多相关信息。

  • 集成开发环境(IDE):Python有各种各样的IDE,而Go语言最初在这方面的支持确实不足。如今,有许多插件可以实现自动补全等功能。在https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins上可以找到插件的完整列表。功能最丰富的是JetBrains公司的GoLand IDE(https://www.jetbrains.com/go/)。

# Java语言

Java是一种强类型的解释型编程语言,由詹姆斯·高斯林(James Gosling)创建。Java的第一个公开版本(Java 1.0)于1995年问世,从那以后,它一直是企业级和Web编程领域的主要编程语言。2006年,太阳微系统公司(Sun Microsystems)开始根据GNU通用公共许可证(GPL)发布Java,而收购了太阳微系统公司的甲骨文(Oracle)以OpenJDK的名义继续推进该项目。

Java取得成功有以下几个原因:

  • 平台独立性:Java源代码被编译成字节码,字节码是一种与目标平台无关的程序表示形式。字节码由Java虚拟机(JVM)解释执行,JVM是Java平台的核心组成部分。这种机制使得程序具有很强的可移植性,打包后的字节码(JAR文件)可以在任何安装了受支持JVM的平台上无需修改即可运行。
  • 面向对象:Java提供了支持面向对象编程的编程结构,包括类、继承、多态和泛型。它还支持诸如面向切面编程等高级编程范式。
  • 自动内存管理:与当时其他流行语言(如C++)不同,Java程序员无需手动管理动态内存。JVM会跟踪内存分配情况,并自动释放那些没有活跃指针指向的对象。JVM的这一组件被称为垃圾回收器,经过多次迭代优化,使得动态内存分配变得更加健壮且对程序的影响最小化。
  • 生态系统:Java生态系统不断发展,拥有大量令人惊叹的库,可以解决几乎任何常见问题,涵盖数据库驱动程序、数据结构和线程支持等方面。同时,Java还有成熟的依赖管理工具,其中最受欢迎的是Maven。Maven本质上是一个构建框架,它不仅描述了Java程序的构建方式,还列出了该程序对其他外部模块和组件的依赖关系。除了依赖项的名称,Maven还会注明版本信息。然后,Maven会从一个或多个代码库(如Maven中央仓库)动态下载这些依赖项的Java库,并将它们存储在本地缓存中。如果你正在构建自己的可复用库,Maven本地缓存也可以更新这些本地项目的JAR文件。所有这些共同构成了一个全面的可复用组件和依赖管理生态系统。
  • 应用框架:Java生态系统率先提出了围绕控制反转(Inversion of Control,IoC)和依赖注入(Dependency Injection,DI)的框架。这些模式允许在不明确提及或实例化具体实现的情况下,连接代码依赖关系。

例如,考虑以下代码片段:

public class ContactsController {
    private ICache contactCache;

    public ContactsController() {
        this.contactCache = new RedisCache();
    }
}
1
2
3
4
5
6
7

在这里,ContactsController是一个联系人API的提供者,为了完成工作,它使用缓存来存储最常访问的联系人。构造函数显式实例化了ICache接口的RedisCache()实现。但这不幸地违背了ICache抽象的目的,因为现在ContactsController与特定的实现紧密耦合。

避免这种情况的一种方法是,让实例化ContactsController类的客户端来提供缓存实现:

public class ContactsController {
    private ICache contactCache;

    public ContactsController(ICache theCache) {
        this.contactCache = theCache;
    }
}
1
2
3
4
5
6
7

乍一看,这解决了耦合问题。但实际上,这将负担转移给了客户端。在许多情况下,客户端并不关心你使用哪种类型的缓存。

IoC/DI模式允许应用框架注入这些依赖关系,而不是采用前面两种方式。Spring就是这样一个流行的Java应用框架,除了依赖注入,它还包含事务支持、持久化框架和消息抽象等其他功能。上述代码在Spring中可以简洁地表示为:

public class ContactsController {
    @Autowired
    private ICache contactCache;

    public ContactsController() {
    }
}
1
2
3
4
5
6
7

这样强大的框架使得编写大规模代码变得更加容易。在这里,Spring的@Autowired注解会搜索与定义匹配的Bean(对象实例),并将其引用注入到ContactsController对象中(ContactsController本身应由Spring管理)。

  • 性能:JVM持续进行多项优化以实现高性能。例如,它包含一个热点(Hotspot)即时(Just - In - Time,JIT)编译器,该编译器可以将频繁执行或对性能关键的字节码指令转换为本机代码指令,避免了原本较慢的解释执行过程。
  • 出色的集成开发环境(IDE)支持:包括开源的Eclipse项目和收费增值的IntelliJ。

这些特性使Java不仅成为后端系统中的主要编程语言,在应用程序(如安卓开发)领域也占据重要地位。

你可能会对IoC模式的名称产生疑问。你或许在想,我们到底在反转什么呢?

为了回答这个名称背后的历史渊源,让我们考虑早期的UI应用程序。它们有一个主程序,用于初始化一系列驱动各个UI元素的组件。主程序负责驱动整个程序,在组件之间传递数据和控制信息。这种编码方式非常繁琐。几年后,UI框架应运而生,它封装了主函数,应用程序开发者只需为各种UI元素提供事件处理程序即可。

这提高了开发者的工作效率,因为他们现在可以专注于业务逻辑,而无需编写繁琐的底层代码。本质上,程序的控制权从开发者编写的代码转移到了框架,这就是所谓的控制反转(IoC)。

就像生活中的所有美好事物一样,Java也有其不足之处(尽管有些观点可能存在争议)。Java生态系统存在一些问题:

  • 冗长性:Java代码往往非常冗长。尽管语言规范中新增了一些特性,如lambda表达式和相关的函数式编程原语,旨在缓解这一问题,但Java代码总体上仍然很冗长。
  • 晦涩性:强大的框架有时会将关键方面进行抽象。例如,Java中新增的并行流(parallel stream)功能,这是为了减少代码冗长而添加的特性之一。但这里有个陷阱,程序中的所有并行流都使用相同的线程池(ForkJoinPool.commonPool)。这个默认设置会导致可扩展性瓶颈,因为代码中的所有并行流都在竞争同一个线程池中的线程!有一种解决方法是定义一个自定义线程池,如下所示:
final List<Integer> input = Arrays.asList(1,2,3,4,5);
ForkJoinPool newForkJoinPool = new ForkJoinPool(5);
Thread t2 = new Thread(() -> newForkJoinPool.submit(() -> {
    input.parallelStream().forEach(n -> {
        try {
            Thread.sleep(5);
            System.out.println("In  : " + Thread.currentThread());
        } catch (InterruptedException e) {
        }
    });
}).invoke());
1
2
3
4
5
6
7
8
9
10
11
  • 复杂性/过度设计:Java强大的构造,包括泛型,有时会导致代码过于精巧,从长远来看难以管理。这包括定义复杂的继承层次结构,这不仅使代码难以阅读和理解,还会由于脆弱的基类问题导致系统变得脆弱。像Spring这样的框架已经变得非常复杂,现在要可靠地使用它们,唯一的方法是使用像Spring Boot这样的“框架之上的框架”(它提供了一组具有合理默认值的可用库,用于各种可调整的设置)。这会创建一组新的线程来运行流的数据计算,但有趣的是,除了新线程池,公共线程池也会被使用。
  • 部署挑战:Java进程所需的资源量并不总是很明确。例如,Java开发者使用-Xmx和-Xms选项(其中mx是堆的最大大小,ms是初始大小)来定义Java堆大小。这个堆用于在Java代码中分配对象。除此之外,Java进程还会使用另一个堆!称为本地堆(Native heap),它是JVM自身需求所使用的堆,包括JIT和NIO等方面。因此,你需要在特定的部署环境中仔细分析应用程序(这在一定程度上削弱了平台独立性这一特性)。
  • 开发者生产力:由于Java代码冗长,并且需要进行部署和测试(编译器很少能捕获到一些棘手的错误),开发者的大量时间都花在了编排/等待开发/测试周期上。虽然像Spring Boot这样的框架有所改善,但与Python或Go相比,生产力仍然存在差距。你需要借助强大的IDE支持才有可能提高效率。

人们想要从Java转向Go的主要原因是资源效率、并发建模和开发者生产力。许多Java纯粹主义者仍然认为Java适用于Web应用程序,而Go适用于系统软件,但在微服务和API驱动开发的世界里,这种区别正在迅速消失。与从Python迁移到Go一样,从Java迁移到Go也并非一帆风顺:

  • 指针:Java程序员通常不习惯处理指针。在使用指针时,他们经常会犯错,例如在决定是按引用还是按值传递变量时(在Java中,所有参数都是按引用传递)。
  • 错误处理:与Python类似,Go语言的错误处理方式以及缺乏try-catch-finally结构,可能会使程序代码变得冗长。如果没有规范地捕获每个函数中的错误,还可能导致程序处理错误的数据。
  • 缺乏泛型:Java程序员已经非常习惯使用泛型(至少是使用泛型,即使不定义泛型),因此需要一段时间来适应Go语言处理相关问题的方式。在这段时间里,开发者可能会感到有些受限。
  • 缺乏类似Spring的IoC框架:Go语言的开发者通常会定义一个主函数来初始化并启动程序中的计算过程。你可以使用init()函数来设计分布式初始化,但在大型程序中,这通常会导致竞态条件,所以最终你还是得编写主驱动程序。
  • new和make:尽管Go语言以简洁著称,但有时也会有一些容易混淆的地方。例如,内置的new()函数用于分配一个类型的零值存储空间(new在许多语言中都是常见的关键字)。Go语言还有make()函数,这是一个特殊的内置分配函数,用于初始化切片、映射和通道。一个常见且严重的错误是,在处理映射等数据结构时使用new而不是make,这会在尝试访问未初始化的映射时引发运行时恐慌(panic)。
  • 性能:虽然Go语言是原生编译的,而Java大多是解释执行的,但目前这两种语言编写的程序在性能上并没有显著差距。这是因为JVM经过了20多年的性能优化,并且采用了即时编译(JIT compilation)等技术。

# 迁移策略

在上一节中,我们了解了人们可能想要迁移到Go语言的一些原因。本节将讨论如何制定将现有代码迁移到Go语言的策略。

# 阶段1 - 学习Go语言

第一步是确保所有开发者都理解Go语言。在大多数项目中,至少会有一些开发者对Go语言并不熟悉。这里所说的理解,不仅是指理论知识,还包括实际尝试各种编程结构,以感受这门语言。https://tour.golang.org/是一个很好的入门资源。《Effective Go》(https://golang.org/doc/effective_go.html)也是学习这门语言的优秀资料,除了语言介绍,它还讨论了最佳实践(包命名约定、状态共享等)。

除了学习语言本身,阅读一些有一定复杂度的Go代码也很有帮助。你可以在GitHub上的许多项目中找到这样的代码,包括Docker的源代码和标准库。我觉得bytes/buffer.go代码(https://golang.org/src/bytes/buffer.go)写得非常好,注释也很详细。

在笔记本电脑上安装Go语言对于所有标准发行版来说都很容易。但我建议使用Go版本管理器(GVM),这样可以更轻松地切换Go版本、GOROOT和GOHOME。

# 阶段2 - 试点服务迁移

一旦你对Go语言有了一定的了解,下一步就是在应用程序的某个服务上进行迁移试点。一个常见的错误是选择最简单的服务进行试点,因为这样无法从试点中充分学习到Go语言的价值。你应该选择一个能够体现Go语言对产品价值的服务。

理想的服务应该涉及大量的并发操作或编排,例如Web门面API。这类服务通常还涉及一些持久化操作,这可以让你亲身体验Go语言与数据库/对象关系映射(ORM)的协同工作。基于API的服务还允许逐步将流量从旧服务切换到新服务,例如,在旅游网站的示例中,你可以将特定城市酒店的流量切换到这个新的服务栈上。

# 阶段3 - 定义包

选择好服务后,下一步是为Go语言版本的项目创建基本的项目布局和包结构。如果当前代码的封装性良好,这应该不会太困难。要注意避免循环导入等问题。最后,你应该得到一个能够描述服务主要组件的包结构。此时也是构建构建工具的好时机,包括编写Makefile和规划依赖管理的相关设置。

# 阶段4 - 移植主程序

现在你可以开始移植服务的主要部分。你应该将所有细节委托给各个包,但主程序应该清晰地描述程序的控制流。例如,在一个REST API服务中,主程序会定义主路由器、健康检查端点,以及为每个资源定义的各种路由器组(类似于Gin框架中的做法),每个资源都在其自己的包中。现在,你最终应该能够运行你的服务了。

此时也是编写测试的好时机。由于大多数包都进行了模拟,预计会有许多测试失败。这是测试驱动开发的核心原则——先编写测试,然后编写代码,直到测试通过。

# 阶段5 - 移植包

阶段4和阶段5需要针对每个包依次进行。

现在必须将我们定义的每个包从原始代码移植到Go语言中。这项工作可以有效地分配给多个开发者,每个团队通常在自己的分支上进行开发。

在这个过程中可能需要做出一些决策,尤其是当原始代码中包含泛型等内容时。在开始编码之前,我们应该列出一些原则。例如,对于泛型,移植的选项包括:

  • 检查泛型类是否真的被使用,或者是否可以简化。通常,泛型代码可能只是早期开发者为了尝试从博客上学到的新技巧而编写的,并非代码的实际需求。
  • 将泛型扩展为具体的实现。如果泛型类用于几个特定目的,那就直接创建两个具体实现。在标准库的strings和bytes包中可以找到实际的例子:它们具有非常相似的API,但实现不同。
  • 使用接口来设计泛型特性。这包括以下步骤:
    • 列出泛型算法或容器所需的操作集。
    • 定义包含这些方法的接口。
    • 为源代码中泛型的每个实例化实现该接口。
  • 使用interface{},这允许我们在引用中存储泛型类型。如果原始泛型不关心存储数据的实际类型,在客户端使用类型断言的接口可以实现类似泛型的Go代码(https://github.com/cookingkode/worktree/blob/master/worktree.go)。这种技术的缺点是我们失去了编译时的类型检查,增加了运行时类型相关故障的风险。
  • 使用代码生成器,有许多工具,如genny(https://github.com/cheekybits/genny),它可以根据模板生成特定类型的代码。

例如,以下代码定义了一个泛型列表类型的模板:

package list

import "github.com/cheekybits/genny/generic"

//go:generate genny -in=template.go -out=list-unit.go gen "Element=uint"

type Element generic.Type

type ElementList struct {
    list []Element
}

func NewElementList() *ElementList {
    return &ElementList{list: []Element{}}
}

func (l *ElementList) Add(v Element) {
    l.list = append(l.list, v)
}

func (l *ElementList) Get() Element {
    r := l.list[0]
    l.list = l.list[1:]
    return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里,Element是泛型类型的标识符。Go generate注释会使Go生成工具在一个名为list-unit.go的单独文件中生成特定单元的代码。生成的代码如下所示:

// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny
package list

type UintList struct {
    list []uint
}

func NewUintList() *UintList {
    return &UintList{list: []uint{}}
}

func (l *UintList) Add(v uint) {
    l.list = append(l.list, v)
}

func (l *UintList) Get() uint {
    r := l.list[0]
    l.list = l.list[1:]
    return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

然后,客户端可以通过正常的导入来使用这个UintList。可以看到,像这样的代码生成器可以让你有效地编写泛型代码。

# 阶段6 - 优化计算

在初次移植时,并发特性可能并不明显。你需要仔细从现有代码中推断或逆向分析出可以并发执行或简化的部分。例如,你可能编写了一系列顺序执行的函数,但实际上这些函数并不需要按照给定的顺序执行,也就是说,它们可以并发运行。在其他情况下,有些代码(比如清理工作)可以在函数返回后再执行。使用defer()关键字可以高效地实现这一点。

在阶段3编写的测试将有助于你避免出现回归问题,这样你就可以对不同程度的并发进行尝试。

了解了迁移策略后,让我们来看看应用程序迁移中最后(但实际上也是最重要的)一部分——组建一个出色的团队来完成这项工作。

# 组建团队

在考虑应用程序迁移时,管理层的一个关键关注点是人才或开发人员的可用性。幸运的是,Go语言非常简单且易于学习,如果你有优秀的工程师,他们很快就能掌握。在“迁移策略”部分的“阶段1 - 学习Go语言”小节中提到了一些学习资源。以我的经验来看,人们在开始实际动手实践一周内就能具备一定的工作能力。

另一个不错的资源是Go语言谚语列表(https://go-proverbs.github.io/ )。它提供了一系列简洁的建议,其中一些,比如“接口越大,抽象越弱”,非常有深度。

不利的一面是,对Go语言程序员的需求很高。所以,一旦你培训了他们,要记得让开发人员保持活力和积极性。在为团队面试开发人员时,对我来说行之有效的方法是招聘那些具备扎实的计算机科学基础和编程能力的人。对于较为复杂的Go程序来说,多线程、死锁和同步原语方面的知识也至关重要。在很多情况下,我发现从非Go语言背景的人员中组建开发团队,教他们Go语言,然后完成工作,并不是一件难事。下面的图表展示了使用不同语言的开发人员的中位数薪资及其编程经验:

img (参考:https://insights.stackoverflow.com/survey/2018 )

# 总结

自诞生近九年以来,Go语言的受欢迎程度持续上升。下面的谷歌趋势图很好地展示了人们对这门语言的兴趣呈指数级增长:

img (参考:https://trends.google.com/trends/explore?date=2009-10-01%202018-07-30&q=golang&hl=en-US )

在本章中,我们探讨了如何学习Go语言、组建团队以及将应用程序迁移到Go语言。

这是我们对Go语言学习的最后一章。希望你享受阅读的过程,并且至少能从中学到一种不同的编程视角。正如艾伦·佩利斯(Alan Perlis)在虽有些年代感但依然相关的《编程警句》(http://pu.inf.uni-tuebingen.de/users/klaeren/epigrams.html )中所说:“一门不会影响你编程思维方式的语言,不值得去了解。”

上次更新: 2025/04/08, 19:40:35
11. 部署规划

← 11. 部署规划

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式