分享一些 Go 在全栈开发中的经验
分享一些 Go 在“全栈”开发中的经验
写这个帖子的目的算是抛砖引玉,分享的部分仅仅是提供一种思路。文章其实写好挺久了,也在团队内部做过分享。只是看到最近关于 Go 的讨论帖子,觉得可以发出来让大家讨论一下。
首先明确一下“全栈”的定义,这里更强调“人”的概念,以 Go 为主要技术栈的个人或者小团队,用尽量少的技能储备,解决尽可能多的开发需求。
这套开发理念帮助我高效率完成了非常多的开发项目,而且创造了非常可观的经济收益,所以我相信这些经验是可以借鉴参考的。当然外包相关的事情重点在于项目而不在于技术,所以这个帖子主要的侧重点会放在 Go 技术栈的开发效率方面。
另外这里提及的需求是广义的功能层面的,而非仅指“网站开发”这种业务层面的。后者解决方案很多,没必要卷……
0x00 一些铺垫与题外话
开头还是要强调一下,这不是 Go 的布道帖,也不是想论证 Go 的优越性。而且以现在的大环境来看,Go 在职场的生存环境也非常一般。我一直以来的核心观点都是:语言是工具,每种语言都有最合适的应用场景。找对工具,解决问题才能事半功倍。
说句题外话,v2ex 经常能看到一些 Java 或者前端味道的 Go 项目,这些项目的共同点是:它们试图去解决一些 Go 领域并不存在的问题。能够看得出背后的开发者仅仅是使用 Go 的语法,而没有学习到语言特有的机制。
这个帖子里讨论的更多的是方法论层面的事情,需要对编程语言的特点、使用场景有比较深刻的认知。涉及到技术细节的地方,很可能需要一定基础才好理解,但是思路方面应该是比较浅显易懂的。
0x10 为什么选择 Go 作为核心技术栈
换句话说,Go 的优势在哪里?以开头“全栈”的定义,分析一下小团队做开发的痛点。
在这方面我的体会是:跨平台、交付便利性、开发速度以及可维护性,排名分先后。
由于不可能投入精力去学习所有的技术,所以核心技术栈一定要相对全面,同时具有天然跨平台的能力,如果有较低的学习成本就更好了。最理想的情况是,它有大厂支持,同时也有比较不错的生态。
其实核心选择无非就是 JS/TS/Node.js Python Java C#/.NET C/C++ 和 Go 这些。这里面 C/C++ 应该是首先排除的,它天然鼓励重复造轮子,即便你有非常深厚的开发经验,在复杂多变的项目需求面前,效率还是不够看。C#/.net 的主要问题是微软和开源,当然有长期经验是可以考虑的。Java 更适合大型团队。
考虑到交付便利性的时候,Go 比起 JS 和 Python 就优势明显了。最最利于的交付模式要么是单可执行文件,要么是服务。如果再加上知识产权保护的需求,编译型语言的优势就更大了。
0x20 以 Go 为核心技术栈的难点
最影响开发效率和体验的肯定是最难解决的那些问题,Go 的弱点或者说不擅长的领域主要是以下方面:
作为带 GC(GarbageCollection) 的语言,不可控的 GC 行为在实时性要求高的场景不如 C/C++
作为强类型( strongly-typed )语言,在字符串处理等领域不如 JS 或者 Python 等等
生态不够完善,经常需要造轮子
不适合 GUI 开发,包括但不限于 Web 框架和跨平台、原生界面
这几个方面我都做过不止一个项目开发,后面结合具体的例子来分析。
0x30 一些 Go 的优势领域案例
0x31 知识产权保护
防破解一直是个很常见的需求,特别是不能做成服务的、或者需要二进制私部署的场景,Go 的编译特性天然就比脚本类语言更好防逆向,脚本类代码是藏不住的。
当然防破解这个事情主要看投入产出比。Go 在这里的优势主要是省心,同时提高了破解的技术门槛。多数时候,写一个时间戳检查、或者 MAC 比对就很够用了。
但在有经验的破解者眼里,这个层面的防护大致就是 JZ/JNZ 的事情,形同虚设。而作为开发者,你可以简单地用“风控”思想给破解者上强度。
这里举个简单的例子:比如你可以分离功能逻辑和校验逻辑,校验失败并不会立即触发异常,而是会正常执行功能逻辑,但附加一个随机延迟的取消机制,使得功能完成之前就异常退出。
上述逻辑用 Go 代码写出来大概就是 contextWithCancel 几行代码的事情。这样产生的程序无论静态反编译还是静态调试都会很令人迷惑,静态方面难以定位校验逻辑,动态方面上下文切换缺少规律,实际运行表现又是难以稳定复现的,大幅增加了逆向难度。
除此之外还可以配合 burrowers/garble 之类的自动化混淆工具。
0x32 统一的跨平台体验
所谓的“统一”有两层含义:一是从开发角度说的,比如我用了十多年 Linux ,所有的开发环境都是 Linux ,基本不需要担心构建产物和运行时问题。(测试还是离不开的,而且交叉编译和 CGO 也有学习门槛,这个放到后面说。)
无论是微软和苹果,都希望把开发者绑定到自己的平台上,这一点我不能忍。
二是基本可以无视底层操作系统级别的抽象,比如线程进程、锁、信号这些,大多数时间 goroutine 的抽象完全够用了。当有需要的时候,这些 OS 相关的抽象逻辑依然可以使用。
举个具体的例子,当你有需要开发一个 GUI 应用的时候,你不需要考虑“UI 线程”、“任务线程”这种问题,完全可以无脑把任务逻辑扔到 goroutine 里面。至于 UI 会不会阻塞、无响应,那是另一个层面的事情,而且实际解决起来非常容易。(这里讨论的 GUI 特指保留( Retained )模式而非立即( Immediate )模式,后者是完全不同的开发范式。)
0x33 互操作性
有一类常见的需求叫“二次开发”,而且很大可能还没有源码。比较多见于工控系统升级,老旧系统自动化等等。
现在习以为常的 API/RPC 模式是互联网时代的标准,放到上个十年,事实上的标准是 ABI/LIB 。一般来说,如果有相应的 DLL/so 文件,做二次开发还是有可能的。
只是做这一类开发需要有比较强的逆向能力,同时对于 C/C++ 和汇编比较熟悉,能够确定清楚各种导出方法的传参。
对于 Go 来说,调用二进制接口其实非常容易。难点仅在于你需要对内存和数据结构有清晰的认识,能够准确使用 unsafe.Pointer 完成数据交换。
反过来通过 Go 的 -buildmode=c-shared 编译,也可以很方便地生成二进制库给其他程序使用。这对于 GUI 的开发是非常有利的,即 Go 既可以通过 API 的方式提供服务,也可以通过 ABI 方式完成嵌入。
这个思想就是用最合适的工具完成它最擅长的事情,剩下的部分交给其它更合适的工具。
0x40 Go 不太擅长的领域以及应对策略
这些案例都是实际项目经历,至少在我看来效果可以接受,不至于想要换技术方案。
0x41 以字符串处理为主的场景
如果不是出于防破解的需要,这一类开发用纯脚本语言更好。如果一定要用 Go 来做,还是有固定应对思路的。首先尽可能结构化,之后在结构化的基础上做处理。
结构化最好的参考例子就是 tidwall/gjson,利用反射和接口实现对任意格式的序列化与反序列化。
至于处理结构化的字符串 grep/awk/sed 绝大多数时间都比自己写要靠谱,用 Go 做这样的外部调用太容易了。
用 Go 做这类开发,还有个好处,就是写出 bug 的概率远低于脚本语言,绝大多数、特别是那种不易察觉的类型、引用错误,都能被 LSP 检查出来。
0x42 高实时性场景
这类场景是 C/C++ 的主战场。用 Go 做类似事情的难点在于 GC 行为不可控。
这里的核心难点在于,你是否有能力做好内存管理。只要你对 C/C++ 那一套足够熟悉,完全可以写出 C/C++ 味道的 Go 代码。语言层面并没有限制你把 Go 当 C/C++ 来用。反过来说 Go 其实省去了非常多麻烦,因为你只需要对核心热点代码进行内存管理,其他部分交给 Go 就可以了。
这里推荐把 google/gopacket 作为学习案例,它的注释非常清晰,而且本质上它的目的就是用 Go 来完成过去一定要 C/C++ 来做的 DPDK 需求。通过这个项目的代码,可以非常直观地学习到 Go 做内存和对象管理的基本思路,以及手动内存管理的高级技巧。
说到底,如果写不好 Go 的高实时性应用,换成 C/C++ 可能也好不到哪里去。瓶颈在于人而不在于语言。
0x43 生态问题
一般说到 Go 的生态问题的时候,主要是说 Go 没有像样的前端 Web 开发框架。在文章开头就已经说过了,这个领域要卷的话是没边的,有远比 Go 更好的选择。
前端轮子多的原因在于,要解决的问题局限在前端这一特定领域。一旦把需求领域拓展出去,前端的轮子根本不够看。
不如回到核心问题上,什么样的轮子是 Go 所欠缺的、同时又是不适合自己造的?
答案是应用算法类。指的是比如 FFT 相关的音频、视频信号处理和格式转换这类,而不是抽象算法,比如快排、矩阵运算这种。这一类轮子自己造门槛太高,但 Go 的标准库一直非常克制,所以需要经常借助开源库。
其实这一类库如果不是太复杂,还是有人愿意用 Go 重写的。稍微复杂一点的,比如 OpenCV 这种就没办法了,重写一遍成本抬到。Go 生态欠佳还是很明显的。
好在这一类需求最难得那部分多数可以用 CGO 来解决,算是弥补了 Go 生态的不足。缺点是你需要自己处理编译依赖等问题,生成的二进制也会明显膨胀,但在可用性面前这些都是小问题。
0x44 前端
如果执意要用 Go 做 Web 开发的话,使用 Go 做后端,然后以 RPC 的方式调用,跟前端解耦是最好的做法。做全栈开发,基础的前端技术还是少不了的。
0x45 GUI
这个问题大概是所有技术栈共同的难点,恰好 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
我虽然没遇到过这类需求,但 Linux 可行方案很多所以就不多说了。另外 TUI 也是可以考虑的。
macOS
目前相对成熟的方案是 progrium/macdriver。
这套方案不仅可用甚至可以打包发行,但我还是推荐用 Objective-C/Swift 做开发。原因是你需要同时在 Objective-C 和 Go 之间做两次内存和对象引用管理,同时也要小小操心一下 UI 线程的问题,不能无脑 goroutine。
换句话说,如果你能轻松搞定这些,做原生开发可能更熟练。
Windows
Windows 用 Go 就友好很多,毕竟 C/C++ 没有 Objective-C 那些特殊机制。
相对成熟的方案有两个,一个是 lxn/walk 另一个是 rodrigocfd/windigo。前者提供了更多非 UI 相关的 Win32 API ,同时也支持 CGO ,后者对于数据结构和常量的封装更完善一些。我更推荐前者作为入门,因为它对 Win32 API 的封装思路更直白,基本可以对照 MSDN 的文档写出一比一的 Go 代码。后者更加现代一点,适合有一定基础然后 fork 一个定制版本给自己用。
比较好的点是这两个库都是以声明式对 Win32 API 做的封装,所以写界面非常直观,结合了声明式和原始拖控件的体验,同时代码量非常低。
在不使用 CGO 的情况下,生成的二进制文件非常小,也不用考虑各种运行时依赖的问题。这是这个方案最大的优点,在简单界面的应用场景里,代码量几乎也是最低的。
这个方案存在一定的学习成本。但以我个人经验来说,真正需要学习的其实只有两个部分,一是 Win32 各种“窗口( Window )”相关的概念,二是事件驱动的消息机制。有其他领域的开发经验的话很容易触类旁通。
0x50 总结
如果你有耐心看到这里,应该就有自己的结论了。
我就补充一点个人的感悟,千万不要手里有把锤子就看什么都是钉子,适合的才是最好的。
wails 不能用吗
#1wails 也是很好的方案。我觉得用不用取决于两点,一看否有意愿去学习,二看能不能接受 webview 作为依赖。
目前 webview 作为依赖的问题是哪些
最近用 wails 写了一个钉钉自动推送群消息,感觉还不错。就是前后端传递数据必须要用结构体,没规划好的话,到处是 type
正确、中肯的文章👍
#3 “webview 依赖的问题”说起来比较复杂,我觉得 webview 是个很好的技术,“问题”只是在于合不合适。笼统地说,浏览器套壳方案的缺点在于性能和打包。性能就是渲染延迟,瓶颈在于 JS 引擎,很难优化到原生水平,所以几乎所有的 webview 应用用户体感迟滞都比较明显。具体到 wails 它在 Windows 上的渲染后端是 WebView2 ,轻微改善了性能和打包问题,但非常有限。具体到需求,webview 方案会有一些不适合的场景。一个是它不适合开发 Immediate 模式的 GUI 应用,比如需要实时按帧渲染的游戏。二是它不适合开发重度依赖系统 API 的平台应用(理论上所有的原生 API 都能调用,但是写那个 COM 接口实在是太恶心了)。反过来,如果这个需求不在乎打包体积,不关注用户体验,也不属于前面说的场景,而你的技术栈恰好又是 Go 和前端,那 wails 其实是非常好的选择。这种情况我更喜欢的方案其实是用 Svelte 糊一个 SPA 出来,然后用 Go 写服务端和业务逻辑。
学习了
多谢分享
多谢分享, 写得很中肯
鉴于你如此优秀,有个项目我想外包给你是否有兴趣?
防破解那一点,以前在看雪看过别人分享反编译的过程,反编译比 c 更简单,目前 go 也没听说有啥成熟的加壳啥的。当然你说和 php js 这种比的话那确实好很多
文章关键字有吗, 我想看下 golang 的反编译思路
很久了,不记得了,大致好像是一个验签还是加密模块,使用 go 写的,其他代码调用。那个作者拿到后分分钟就反编译破解了。说 go 生成的汇编代码比较死板,一眼就明白咋回事,不像 c 编译器优化特别多
#11 文章里逆向那段主要是想表达一个意思:防破解本质上是个投入产出比的事情。Go 加上十分钟就能学会的小技巧,只需要几行代码和一点点自动化工具,足以为价值几万元的业务提供防御。我给这个十分钟小技巧,其实包含了动态、静态、控制流和混淆多个层面的防护手段。强度差不多等同于各类动态语言的 vmp 水平。对于绝大多数开发者来说,如果不是有丰富的对抗经验,是很难写出来真正有效的防破解逻辑的。从专业逆向者的角度上说,过高的门槛本就拦住了绝大部分人,甚至轮不到考虑投入产出比的事情。
#12 对于去掉符号表、字符串混淆之后的 go 二进制,基本只能硬怼汇编,参考对应版本 runtime 的实现来逆向。不是专门做这个的,可能连 main 入口都找不到。基本上黑产都喜欢拿 go 过免杀。常规没有防逆向措施的 go 二进制就和 c 逆向差不多。
😂每次用 Go/Dart 写 Windows API 的时候每次都在想,要不努力点学会 Rust 算了。万恶的 Windows😤
中肯的,感谢分享
#16Windows API 难用某种程度上说是微软故意的。我没做过特别专业的 Windows 开发,这是我个人的推测。
总结的挺好。 我从 Python 转 Go 觉得 Golang 是个性价比(或投资回报)比较高的语言。Web 开发用 Python ,高性能部分用 Go 做 bindings (或者微服务)+ k8s ,我觉得应届生都能从容应对 c100k 。
非常棒,感谢分享
有用的分享
真的很不错的分享,谢谢!
#19确实是这个意思,Go 的性价比很高。另外 cNk 这个说法我感觉好多年没见过了,N=1 的时候算是个硬件问题,N=10 的时候算是技术问题,N=100 大概更接近于工程问题了。我在正文没有提到 Go 带来的另一个好处,主要原因是这个体验比较主观。我确实认为 Go 的项目很有利于长期维护。最明显的一点是,即使我现在回看两年前的项目,不管文档、注释写得如何,几乎很快都能回顾起来。看 GitHub 的开源项目也有同感,几乎都能非常快地定位到关键代码。核心原因应该是得益于 Go 的两个优秀设计,一个是 share by communicating
的 concurrency 模型,另一个是 accepting interfaces, return structs
的解耦机制。在 Go 之前,想要轻松写出正确的多线程代码是非常困难的事情,想轻松写出扩展性强的代码也需要有深厚的功底。
干货很多的分享
高性能应用里,因为 GC 问题而弃用 Go ,可参考真实案例: discord.com/blog/why-discord-is-switching-from-go-to-rust
你用 Go 的范围挺广的,我只用来开发接口和服务。我从 Android 开发转过来,一个客户端开发能顺利转型还得多亏了 Go 简洁的语法特性、极其容易的部署、不用学框架直接标准库直接上、对接各种后端开发常见的组件都有 client 库使用也简单、转型时正好是 Go 采用率上升期的前期,比 Java 更容易得到工作机会。要是转 Java 后端阵营去卷,不知道能不能转成功,拥有 Thinking in Java 多年却从未能看完。当然,我当年觉得 Java 难肯定是心智发展不成熟,自觉是个晚熟的人。现在有信心学会使用 Java ,但已没有了动机,除非有人愿意支付离谱的钱让我干这事 😂。经过几年的摸爬滚打,使用 Go 开发 REST 、GraphQL 、gRPC 等接口和服务越来越熟悉,相关的工程化也越来越熟练了,内心也不再担忧自己是 App 出身入行而觉得自己的后端技能不如同事了。与此同时也经常看到有人说 Go 生态不完善(我一个业务仔没觉得有这困扰,后端开发用到的组件都有 client 库);见惯了其他语言的程序员学习 Go 时对它很有成见,不愿意接受它是一门跟其他语言不同的语言(这个道理很简单,就像语言们有不同的名字一样,语言们就是不同!);我已经用 Go 给 5 个雇主工作过了,仍然有人说推荐 Go 开发后端不是蠢就是坏。这些倒提醒了我,如果我有必要学习其它语言去完成事情时一定不能埋怨,而是要尽快成为那门语言的“说母语的人”。
#25实时性要求高的场景,无 GC 永远都比有 GC 优秀。对于 GC 来说,最难处理的情形要么是碎片化分配,要么是海量引用。我的贴子里提到的技巧主要是解决前者,后者是很难在代码层面上有所改善的,也是 Go 原理上做不到的。还是那句话,技术栈该换就换。虽然 discord 技术博客写得很好,但是软件体验仍旧是一坨……
cgo is not go
#28在 Go 的生态里 Cgo 的意义非常重要。做个不那么恰当的类比,游戏行业 Unity/C#/Lua 生态很大程度上就是因为 Lua 和 C 互通能力。还是回到我正文里提到的 OpenCV 的例子,在 CV 领域几乎没有完全的替代。不得不用的情况下,除非用 C++ 开发,其他语言想要使用 OpenCV 都要有相应的 bindings 。开发这种 bindings 本身就是很复杂的事情,这里 Cgo 的优势就显现出来了。简单的项目不用写 bindings 就可以直接用,复杂的项目通过 Cgo 可以高度自动化 bindings 的生成过程。某种程度上可以认为,C 生态完全可以为 Go 所用。正文里提到过,Go 欠缺的恰恰就是这一类的轮子。
.net 部分的知识可以更新一下
正确的 中肯的
有用
前言 接触 servicemesh 有一段时间了,越来越觉得这个东西在当前场景下是个可有可无的东西。 于是问了下 GPT ,servicemesh 解决了啥问题,以下是他的回答…
来源:http://blog.seattleinterviewcoach.com/2009/02/140-google-interview-questions.html(墙) …
如题,刚开始学习 flutter ,就听到了这个消息 小打小闹,说是 前景堪忧 谷歌只是主导,更多还是很多大佬参与开发维护的,这个不用担心吧 会让开发者担心 flut…