Go
在“全栈”开发中的经验写这个帖子的目的算是抛砖引玉,分享的部分仅仅是提供一种思路。文章其实写好挺久了,也在团队内部做过分享。只是看到最近关于 Go
的讨论帖子,觉得可以发出来让大家讨论一下。
首先明确一下“全栈”的定义,这里更强调“人”的概念,以 Go
为主要技术栈的个人或者小团队,用尽量少的技能储备,解决尽可能多的开发需求。
这套开发理念帮助我高效率完成了非常多的开发项目,而且创造了非常可观的经济收益,所以我相信这些经验是可以借鉴参考的。当然外包相关的事情重点在于项目而不在于技术,所以这个帖子主要的侧重点会放在 Go
技术栈的开发效率方面。
另外这里提及的需求是广义的功能层面的,而非仅指“网站开发”这种业务层面的。后者解决方案很多,没必要卷……
开头还是要强调一下,这不是 Go
的布道帖,也不是想论证 Go
的优越性。而且以现在的大环境来看,Go
在职场的生存环境也非常一般。我一直以来的核心观点都是:语言是工具,每种语言都有最合适的应用场景。找对工具,解决问题才能事半功倍。
说句题外话,v2ex
经常能看到一些 Java
或者前端味道的 Go
项目,这些项目的共同点是:它们试图去解决一些 Go
领域并不存在的问题。能够看得出背后的开发者仅仅是使用 Go
的语法,而没有学习到语言特有的机制。
这个帖子里讨论的更多的是方法论层面的事情,需要对编程语言的特点、使用场景有比较深刻的认知。涉及到技术细节的地方,很可能需要一定基础才好理解,但是思路方面应该是比较浅显易懂的。
Go
作为核心技术栈换句话说,Go
的优势在哪里?以开头“全栈”的定义,分析一下小团队做开发的痛点。
在这方面我的体会是:跨平台、交付便利性、开发速度以及可维护性,排名分先后。
由于不可能投入精力去学习所有的技术,所以核心技术栈一定要相对全面,同时具有天然跨平台的能力,如果有较低的学习成本就更好了。最理想的情况是,它有大厂支持,同时也有比较不错的生态。
其实核心选择无非就是 JS/TS/Node.js
Python
Java
C#/.NET
C/C++
和 Go
这些。这里面 C/C++
应该是首先排除的,它天然鼓励重复造轮子,即便你有非常深厚的开发经验,在复杂多变的项目需求面前,效率还是不够看。C#/.net
的主要问题是微软和开源,当然有长期经验是可以考虑的。Java
更适合大型团队。
考虑到交付便利性的时候,Go
比起 JS
和 Python
就优势明显了。最最利于的交付模式要么是单可执行文件,要么是服务。如果再加上知识产权保护的需求,编译型语言的优势就更大了。
Go
为核心技术栈的难点最影响开发效率和体验的肯定是最难解决的那些问题,Go
的弱点或者说不擅长的领域主要是以下方面:
C/C++
JS
或者 Python
等等这几个方面我都做过不止一个项目开发,后面结合具体的例子来分析。
Go
的优势领域案例防破解一直是个很常见的需求,特别是不能做成服务的、或者需要二进制私部署的场景,Go
的编译特性天然就比脚本类语言更好防逆向,脚本类代码是藏不住的。
当然防破解这个事情主要看投入产出比。Go
在这里的优势主要是省心,同时提高了破解的技术门槛。多数时候,写一个时间戳检查、或者 MAC 比对就很够用了。
但在有经验的破解者眼里,这个层面的防护大致就是 JZ/JNZ
的事情,形同虚设。而作为开发者,你可以简单地用“风控”思想给破解者上强度。
这里举个简单的例子:比如你可以分离功能逻辑和校验逻辑,校验失败并不会立即触发异常,而是会正常执行功能逻辑,但附加一个随机延迟的取消机制,使得功能完成之前就异常退出。
上述逻辑用 Go
代码写出来大概就是 contextWithCancel
几行代码的事情。这样产生的程序无论静态反编译还是静态调试都会很令人迷惑,静态方面难以定位校验逻辑,动态方面上下文切换缺少规律,实际运行表现又是难以稳定复现的,大幅增加了逆向难度。
除此之外还可以配合 burrowers/garble
之类的自动化混淆工具。
所谓的“统一”有两层含义:一是从开发角度说的,比如我用了十多年 Linux ,所有的开发环境都是 Linux ,基本不需要担心构建产物和运行时问题。(测试还是离不开的,而且交叉编译和 CGO 也有学习门槛,这个放到后面说。)
无论是微软和苹果,都希望把开发者绑定到自己的平台上,这一点我不能忍。
二是基本可以无视底层操作系统级别的抽象,比如线程进程、锁、信号这些,大多数时间 goroutine
的抽象完全够用了。当有需要的时候,这些 OS 相关的抽象逻辑依然可以使用。
举个具体的例子,当你有需要开发一个 GUI 应用的时候,你不需要考虑“UI 线程”、“任务线程”这种问题,完全可以无脑把任务逻辑扔到 goroutine
里面。至于 UI 会不会阻塞、无响应,那是另一个层面的事情,而且实际解决起来非常容易。(这里讨论的 GUI 特指保留( Retained )模式而非立即( Immediate )模式,后者是完全不同的开发范式。)
有一类常见的需求叫“二次开发”,而且很大可能还没有源码。比较多见于工控系统升级,老旧系统自动化等等。
现在习以为常的 API/RPC 模式是互联网时代的标准,放到上个十年,事实上的标准是 ABI/LIB 。一般来说,如果有相应的 DLL/so 文件,做二次开发还是有可能的。
只是做这一类开发需要有比较强的逆向能力,同时对于 C/C++
和汇编比较熟悉,能够确定清楚各种导出方法的传参。
对于 Go
来说,调用二进制接口其实非常容易。难点仅在于你需要对内存和数据结构有清晰的认识,能够准确使用 unsafe.Pointer
完成数据交换。
反过来通过 Go
的 -buildmode=c-shared
编译,也可以很方便地生成二进制库给其他程序使用。这对于 GUI 的开发是非常有利的,即 Go
既可以通过 API 的方式提供服务,也可以通过 ABI 方式完成嵌入。
这个思想就是用最合适的工具完成它最擅长的事情,剩下的部分交给其它更合适的工具。
Go
不太擅长的领域以及应对策略这些案例都是实际项目经历,至少在我看来效果可以接受,不至于想要换技术方案。
如果不是出于防破解的需要,这一类开发用纯脚本语言更好。如果一定要用 Go
来做,还是有固定应对思路的。首先尽可能结构化,之后在结构化的基础上做处理。
结构化最好的参考例子就是 tidwall/gjson
,利用反射和接口实现对任意格式的序列化与反序列化。
至于处理结构化的字符串 grep/awk/sed
绝大多数时间都比自己写要靠谱,用 Go
做这样的外部调用太容易了。
用 Go
做这类开发,还有个好处,就是写出 bug 的概率远低于脚本语言,绝大多数、特别是那种不易察觉的类型、引用错误,都能被 LSP 检查出来。
这类场景是 C/C++
的主战场。用 Go
做类似事情的难点在于 GC 行为不可控。
这里的核心难点在于,你是否有能力做好内存管理。只要你对 C/C++
那一套足够熟悉,完全可以写出 C/C++
味道的 Go
代码。语言层面并没有限制你把 Go
当 C/C++
来用。反过来说 Go
其实省去了非常多麻烦,因为你只需要对核心热点代码进行内存管理,其他部分交给 Go
就可以了。
这里推荐把 google/gopacket
作为学习案例,它的注释非常清晰,而且本质上它的目的就是用 Go
来完成过去一定要 C/C++
来做的 DPDK 需求。通过这个项目的代码,可以非常直观地学习到 Go
做内存和对象管理的基本思路,以及手动内存管理的高级技巧。
说到底,如果写不好 Go
的高实时性应用,换成 C/C++
可能也好不到哪里去。瓶颈在于人而不在于语言。
一般说到 Go
的生态问题的时候,主要是说 Go
没有像样的前端 Web 开发框架。在文章开头就已经说过了,这个领域要卷的话是没边的,有远比 Go
更好的选择。
前端轮子多的原因在于,要解决的问题局限在前端这一特定领域。一旦把需求领域拓展出去,前端的轮子根本不够看。
不如回到核心问题上,什么样的轮子是 Go
所欠缺的、同时又是不适合自己造的?
答案是应用算法类。指的是比如 FFT 相关的音频、视频信号处理和格式转换这类,而不是抽象算法,比如快排、矩阵运算这种。这一类轮子自己造门槛太高,但 Go
的标准库一直非常克制,所以需要经常借助开源库。
其实这一类库如果不是太复杂,还是有人愿意用 Go
重写的。稍微复杂一点的,比如 OpenCV 这种就没办法了,重写一遍成本抬到。Go
生态欠佳还是很明显的。
好在这一类需求最难得那部分多数可以用 CGO 来解决,算是弥补了 Go
生态的不足。缺点是你需要自己处理编译依赖等问题,生成的二进制也会明显膨胀,但在可用性面前这些都是小问题。
如果执意要用 Go
做 Web 开发的话,使用 Go
做后端,然后以 RPC 的方式调用,跟前端解耦是最好的做法。做全栈开发,基础的前端技术还是少不了的。
这个问题大概是所有技术栈共同的难点,恰好 V2EX 前两天有个帖子 /t/992582 就在讨论用什么写 GUI 最快,可以做个参考。
情感上我是支持原生的,实践里我也是这么做的,但跨平台确实有其适合的应用场景。通常做产品的(研发)会倾向跨平台方案,做功能的(外包)往往都有特定的使用场景,取决于是否有真正的跨平台需求。
这里有个分水岭,就是应用本身是否重度依赖操作系统的 API 。如果依赖程度很低,跨平台方案会方便一点,如果重度依赖系统 API 的话,原生方案更好。无论哪种跨平台方案,终究会遇到它没有封装特定 API 而你又不得不用的时候。
最明显的就是需要和其他应用进行交互的场景,基于 GUI 自动化的需求,涉及到的系统 API 就很难用 Electron 之类的跨平台方案来实现调用。理论上可以,但没必要
另一个参考依据是 UI 复杂度,这里复杂度通常有两方面,一是 UI 层级( Hierarchy )的多少,二是动态添加或删除操作 UI 元素的多少。复杂到一定程度就需要考虑更换技术栈了。
跨平台必然涉及到其他的技术栈,这里就不展开了,主要讨论原生方案。
由于各个操作系统都不是用 Go
写的,所以只能通过 ABI 的形式来调用系统库完成 UI 构建。这个过程就需要各个平台系统库的 Go bindings
,通常这些接口会比较多,单独使用 syscall
转换不现实而且存在效率问题,所以几乎全都依赖 CGO 实现。
随便一提,自动化生成这类 ffi
其实是比较麻烦的,相关开源项目的开发者都是好人。诸如复合数据结构映射、循环依赖解析多数只能靠人来完成。
原生开发的难点在于,一定要懂得原生开发。这也恰恰是很多人选择跨平台的原因,因为不会啊……相比原生 GUI 开发,写 web 前端可简单太多了。关键是跨平台方案的界面抽象和布局模型确实要友好得多但原生不会的话真就没得选,单独学习就要考虑投入产出的问题
技术层面上,调用二进制 ABI 需要对内存、指针等底层数据结构要了解得比较透彻。原因是 Go
和 C
的原生数据结构并不是一一对应的,同时传参和返回也多数是指针,以及 UTF-8/UTF-16 这样的编码区别。
这里有个题外话,就是声明式和命令式。因为编程范式的差异,Windows/macOS 都是以新 API 的形式提供的,即 Win32/WinRT 和 Cocoa/SwiftUI 这两套。但仅就能够实现的功能来说,旧的 API 是完全够用的。
现在要用 WinRT/SwiftUI 基本只能用官方支持的语言开发,只用 Win32/Cocoa 还是很方便的。这些旧 API 在设计时还没有沙盒等概念,其实易用性方面反倒更好一点。以后可能会有人做新 API 相关 bindings 的开发,但这个事情工作量真的很大。
我虽然没遇到过这类需求,但 Linux 可行方案很多所以就不多说了。另外 TUI 也是可以考虑的。
目前相对成熟的方案是 progrium/macdriver
。
这套方案不仅可用甚至可以打包发行,但我还是推荐用 Objective-C/Swift
做开发。原因是你需要同时在 Objective-C
和 Go
之间做两次内存和对象引用管理,同时也要小小操心一下 UI 线程的问题,不能无脑 goroutine
。
换句话说,如果你能轻松搞定这些,做原生开发可能更熟练。
Windows 用 Go
就友好很多,毕竟 C/C++
没有 Objective-C
那些特殊机制。
相对成熟的方案有两个,一个是 lxn/walk
另一个是 rodrigocfd/windigo
。前者提供了更多非 UI 相关的 Win32 API ,同时也支持 CGO ,后者对于数据结构和常量的封装更完善一些。我更推荐前者作为入门,因为它对 Win32 API 的封装思路更直白,基本可以对照 MSDN 的文档写出一比一的 Go
代码。后者更加现代一点,适合有一定基础然后 fork 一个定制版本给自己用。
比较好的点是这两个库都是以声明式对 Win32 API 做的封装,所以写界面非常直观,结合了声明式和原始拖控件的体验,同时代码量非常低。
在不使用 CGO 的情况下,生成的二进制文件非常小,也不用考虑各种运行时依赖的问题。这是这个方案最大的优点,在简单界面的应用场景里,代码量几乎也是最低的。
这个方案存在一定的学习成本。但以我个人经验来说,真正需要学习的其实只有两个部分,一是 Win32 各种“窗口( Window )”相关的概念,二是事件驱动的消息机制。有其他领域的开发经验的话很容易触类旁通。
如果你有耐心看到这里,应该就有自己的结论了。
我就补充一点个人的感悟,千万不要手里有把锤子就看什么都是钉子,适合的才是最好的。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.