你问我为什么值得专门花时间了解Go语言?我直接说结论:因为整个软件行业的底层基础设施,正在全面转向Go。这不是某个小圈子的热闹,而是持续了很多年的结构性变化。最近两年我面试后端候选人,超过一半的简历里写着“熟悉Go”或“用过Go”;身边做云原生、微服务、中间件、运维自动化的朋友,几乎人人都在写Go。
这篇文章我想从一个一线从业者的角度,把Go语言到底是什么、它解决了什么问题、学完之后能获得什么、以及怎么用最短时间上手,一次讲透彻。我不想给你列一堆空泛的优点清单,那些官网和网课摘要早就写满了。我想讲的,是那些真正影响你决策、也会在你学习和工作中反复遇到的底层逻辑。
不管你是准备入行的学生、正考虑技术方向切换的在职开发者,还是技术管理者想评估团队技术栈,这篇文章都值得看完。读完之后你会对Go的全貌有个真实判断,也能直接照着第4章的路径开始第一天的学习和编码实践。
1. 为什么是Go语言:它解决的不是小问题
1.1 并发编程的复杂度:传统方案的两条死路
服务端开发绕不开一个词:高并发。一个后端系统同时要处理成千上万的网络连接和请求,这是常态而不是例外。十年前大家解决高并发,基本只有两条路:多线程和异步回调,而这两条路走深了都很难受。
先看多线程。线程是操作系统级的资源,创建和销毁都要走内核,每次上下文切换都有代价。更麻烦的是内存占用,一个线程默认栈空间在MB级别,你开几百个线程可能还顶得住,想开上万个?内存直接爆掉。而一旦涉及多个线程共享数据,加锁、死锁、条件变量、原子操作这些知识点就会全部涌上来,任何一个环节出了问题都是线上事故级别。我用个生活里的类比:多线程开发就像几十个人挤在一间屋子里,每个人还要扛一张固定的大床进来,然后互相协调谁用床、谁让路,不吵起来才怪。
再看异步回调。为了绕开线程开销,很多人选择固定几个线程,用事件循环加回调机制来推动逻辑。这样确实能降低资源消耗,但代码会被拆得稀碎。一个业务逻辑,前半段在回调A里,后半段在回调B里,中间还夹着各种状态变量。你写的时候必须把整个流程在脑子里拼图一样拼起来,排查问题的时候更是痛苦,看代码像在写侦探小说。
Go的出现,恰恰是把这两条路的痛点一起解决掉。它用同步的代码风格写出并行的效果,你不用再整天跟回调地狱搏斗,也不用自己管理复杂的线程池。这是它最核心的吸引力,也是很多人学习后回不去的根本原因。
1.2 Go的定位:站在系统编程和业务开发之间
Go语言诞生于2009年,背景很实际:某搜索巨头的几位系统软件工程师,长期被海量并发服务的开发效率折磨。他们用C++写底层系统,开发速度慢、编译久、人手不够用;用脚本语言做业务,性能和并发支持又跟不上需求。他们需要一门新的语言,最好能同时拥有C++级别的运行效率、Python级别的开发速度,再加上一个现代语言该有的标准库和工具链。
所以Go的定位从一开始就不是“取代一切”,而是把系统软件领域的开发效率和运行稳定性做一个更好的均衡。C++像专业赛车,性能很强但难驾驭;脚本语言像舒适家用车,好开但跑不了硬派路;Go像四驱SUV,能跑快、能拉货,平时开着还不累。它允许你写高性能的服务器程序,但语法上又简单到一周就能上手,不需要为编译、链接、内存管理这些底层细节操太多的心。
这套设计思路直接影响了后来的一切:为什么Go的代码读起来那么舒服、为什么它的编译速度那么快、为什么它能把并发内置进语言而不是靠第三方库。这些都不是偶然,而是设计者从真实痛点出发做出的取舍。
1.3 学习Go能换来什么:收益拆解
我可以很直白地列一张对比表,把Go和另外几门主流语言放在几个关键维度上做对比:
| 维度 | Go | Java | Python | C++ |
|---|---|---|---|---|
| 语法复杂度 | 极简 | 中等偏上 | 简单 | 很高 |
| 并发模型 | goroutine内置 | 线程池/并发工具 | 受制于GIL,需多进程 | 原生线程 |
| 部署产物 | 单个静态二进制 | 需要JRE和JAR包 | 需要解释器与依赖 | 二进制加运行库 |
| 启动速度 | 很快 | 中等 | 较慢 | 很快 |
| 垃圾回收 | 内置 | 内置 | 内置 | 手动管理为主 |
这些差异在今天尤其重要。云原生和容器化早就成了后端部署的主流方式,而Go的静态二进制、低内存占用、极快启动速度,几乎是为容器世界量身定做的。微服务很多,每个服务要快速启动、独立部署、随时扩缩容,Go在这些维度上的表现都远超Java和Python。
再说学习收益。第一次是说就业,后端领域里云原生、中间件、基础设施方向的岗位,越来越多地要求懂Go,具备Go语言能力和容器化部署经验的人在市场上竞争力很强。第二次是学习成本,Go的语法规模很小,两三个星期就能开始写项目,不需要花大半年啃完框架和设计模式才能干活。第三次是团队协作,因为语言表达力简洁,看别人写的Go代码几乎不费力,这对需要长期维护的团队来说价值非常高。
请记住一点:不要抱着“学了Go就要替代Java或Python”的对抗心态。更健康的角度是,语言是工具,多一门顺手且能写底层基础服务的手艺,你的选择面就大了很多。Go不是一个锦上添花的玩具,一个后端开发者同时熟悉Go和另一门主流语言,是现在非常常见的技能组合。
2. Go语言的核心特性与底层原理
2.1 goroutine:两核与两万线程的秘密
goroutine是Go语言并发的核心组织单位,理解它就理解了一半Go。操作系统线程为什么重?因为创建、调度、销毁都要内核参与,默认栈空间又很大,线程数量一高内存和调度开销就失控。而goroutine是Go运行时自己调度的“轻量级协程”,你写代码时像起了个线程,但它的栈初始只有几KB,按需增长,创建成本接近一个函数调用的开销。
Go运行时内部有一套调度机制,简单来说就是把海量的goroutine通过逻辑处理器,分发到少数几个操作系统线程上去执行。线程数可能只有几十个,但goroutine可以同时存在几万甚至几十万个。这就像什么呢?传统线程是你给每个工人配一套固定的大房子,住一个工人就要占用一大块地;goroutine是你们公司统一管理的一群灵活工人,谁有活谁去干,没有活就在休息区待命,整个仓库能容纳的人数比固定住房多得多。
一个很直观的例子说明goroutine有多容易用:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Printf("goroutine #%d 正在执行\n", n) }(i) } wg.Wait() fmt.Println("所有 goroutine 执行完毕") }这里用go关键字就把一个函数放到独立的执行体里跑,然后用sync.WaitGroup等待所有goroutine完成。你不需要关心线程怎么分配、栈怎么增长,这些都由运行时搞定。
有一个细节要提醒:在Go 1.22之前,循环变量直接传给闭包存在经典的捕获坑,直接写go func() { fmt.Println(i) }()会看到多个goroutine打印相同的值。老版本Go里必须像上面那样把i作为参数传入,新Go版本虽然已经修复了这个语义,但养成显式传参的习惯依然是好习惯,代码意图更清晰。
2.2 channel:goroutine之间的数据高速公路
光有goroutine还不够,协程之间的数据同步才是真正容易出错的地方。Go给出的答案是channel。它的核心理念被概括成一句话:不要通过共享内存来通信,而应该通过通信来共享内存。这句话初读有点绕,我用例子解释。
channel就像一条管道,一端往里面投数据,一端从里面取数据。根据容量不同分为两种:
- 无缓冲channel:投递方与接收方必须同时准备好,任何一方不在都会阻塞等待,相当于两个人站在对讲机前面,必须同时按着通话键才能完成对话。
- 有缓冲channel:像信箱,投递方把信放进箱子里就可以离开,接收方有空就来取。缓冲大小是有限的,信箱满了投递方就得等。
一个典型的生产者消费者例子:
package main import ( "fmt" "sync" ) func producer(ch chan<- int, wg *sync.WaitGroup) { defer wg.Done() for i := 1; i <= 5; i++ { fmt.Printf("投递 %d\n", i) ch <- i } close(ch) } func consumer(ch <-chan int, wg *sync.WaitGroup) { defer wg.Done() for v := range ch { fmt.Printf("接收 %d\n", v) } } func main() { ch := make(chan int, 3) var wg sync.WaitGroup wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() }注意代码里的几个小设计:生产者用close(ch)关闭channel,消费者用for v := range ch遍历,直到channel被关闭才退出循环。chan<-和<-chan分别表示单向发送和单向接收的channel,在函数签名里声明方向能让编译器帮你保证数据流向不出错。
我踩过最多的坑恰好都跟channel相关:向一个已关闭的channel发送数据会直接panic;重复关闭channel也会panic;读取一个永远不关闭的channel会让接收方永远等待,造成goroutine泄漏。这些都是初学阶段的高发问题,我的经验是:谁生产数据谁负责关闭channel,接收方不要关闭;关闭后不要再往里写数据;用完的channel如果不再需要接收,考虑用超时或Context的方式解除阻塞。
2.3 垃圾回收、静态编译与部署体验
Go自带垃圾回收,这点和Java类似,开发时不需要手动管理内存。很多从C++转过来的人第一反应是舒适,不用担心忘了释放内存,也不用反复检查悬空指针。Go的GC停顿已经优化得非常短,大多数业务场景根本感知不到,这也是它能在保留开发效率的同时维持高性能的原因。
真正让我从“喜欢”变成“很常用”的,是Go的部署体验。Go编译出来是一个静态链接的二进制文件,直接把它扔到服务器上就能跑,不需要安装运行时环境,不需要配置类路径,更不需要处理各种依赖冲突。对比一下:Java程序上线要先装JDK,调一堆启动参数;Python程序上线要先装解释器,再搞定包依赖;Go这边就是一个文件,上传、给权限、运行,完事。
尤其在容器化场景里,这个优势被放得很大。基础镜像不需要包含运行环境,二进制文件只有几十MB甚至几MB,镜像体积小、启动快、内存占用低,扩缩容效率高。你可以用一条命令把当前平台的东西交叉编译到其他平台:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o app-linux .交叉编译是Go的看家本领,在Mac上编译出Linux服务器上能跑的二进制,一条命令搞定。这也是大量运维工具和基础设施组件选择Go的直接原因之一:作者只需要发一个二进制文件,用户拿来就能用。
3. Go语言真正发光的四个应用场景
3.1 后端API与微服务
Go在内置标准库里就提供了开箱可用的高性能HTTP服务模块。你不需要引入一堆第三方依赖,就能在几行代码内起一个Web服务:
package main import "net/http" func main() { http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) http.ListenAndServe(":8080", nil) }这看着简单,但生产级后端光靠框架语法还不够。实际项目里需要考虑中间件、路由拆分、参数校验、日志追踪、配置中心、服务注册发现,这些生态组件在Go里已经发展得相当齐全。为什么微服务架构这么偏爱Go?因为微服务的本质是许多独立部署的小进程,而Go的进程体积小、启动速度快、内存占用低,天然适合容器编排环境里的频繁创建和销毁。你同时跑几十个微服务,如果每个都占用几百MB内存,成本会非常夸张;用Go写,内存占用小一个数量级,扩容也更轻松。
我建议第一次接触Go后端的读者,先别急着上重型框架,老老实实用标准库写一个HTTP服务,感受一下它的简洁。然后再引入路由中间件和依赖注入,你会更容易理解框架到底帮你解决了什么问题。
3.2 云原生与基础设施
云原生是这几年的技术主线,而Go在这一领域地位特殊。云原生不是某个具体的软件,而是围绕容器、动态编排、微服务、DevOps的一套研发理念,几乎所有支撑这套理念的底层基础设施组件都与Go有直接关系。我刻意不在这里点名具体产品,但你只接触一圈云原生生态就会发现:容器运行时、容器编排、服务发现、配置中心、监控告警、日志采集、网关代理、服务网格,大量核心组件从诞生起就是用Go写的,剩下的也大多提供Go语言的官方开发包。
为什么偏偏是Go?因为基础设施软件对运行资源极为敏感,它通常是7x24小时常驻服务,CPU和内存的每一分浪费都会被放大。Go的并发模型正好匹配这些组件的典型工作负载:大量连接同时进来、每个连接独立处理、偶尔需要跨节点通信。另外,Go的交叉编译能力让基础设施作者可以轻松交付多平台二进制,这也是它在开源工具圈受欢迎的重要因素。
如果你是做后端开发的,未来大概率会直接操作或二次开发这些基础设施组件。想真正理解它们的实现原理、甚至贡献代码,Go就是绕不开的阅读语言。哪怕只读不写,对Go的熟悉程度也会决定你能在源码里走多远。
3.3 中间件与命令行工具
中间件是一门很考验并发能力的手艺。网络代理、消息队列客户端、缓存代理、流量网关,这些程序最明显的特点就是高吞吐、长连接、低延迟。Go的goroutine-per-connection模型让这类程序写起来非常自然,每一个连接对应一个goroutine,逻辑上互不干扰,底层又有统一的调度器在管理,开发体验接近同步代码,但运行效率非常高。
命令行工具则是另一片非常适合Go的领域。运维人员每天都在跟各种CLI工具打交道,这些工具用Go写有几个天然的好处:编译成单个二进制,不依赖用户电脑有没有装解释器;交叉编译简单,Linux、Windows、macOS三平台随便发布;启动速度极快,用户点下去瞬间就能看到帮助信息。如果你有这样的需求:写一个小工具分发给几百个同事使用,Go二进制绝对比让他们安装Python环境和一堆依赖省心得多。
实际项目里有大量这种“看起来不起眼但非常修炼内功”的应用,比如配置检查工具、日志分析小程序、批量脚本替代工具。拿Go来写,开发速度快,交付又干净。
3.4 网络编程与高并发长连接
Go最让我惊艳的场景,其实是网络编程。TCP长连接、WebSocket推送、自定义协议网关、游戏服务端、即时通讯后端,这些领域以前用Java或C++写都挺折腾,而Go让“每个连接一个goroutine”成为最自然的实现方式。
举个例子,你想实现一个TCP服务器,监听端口后每来一个连接就启动一个goroutine去处理,代码量非常小。因为goroutine足够轻量,你不需要像传统线程那样担忧资源耗尽的临界值。一个进程内同时维护几万个长连接在Go中是很常规的操作,这在Java里需要精心设计线程池模型,在C++里更是要操心各种内存问题。
当然,goroutine可以随便起不代表你可以真的一点不控制。后面我会专门聊到,无序地创建goroutine而没有正确的退出机制,是线上服务内存暴增的常见元凶之一。学会“并发很好用”和“如何优雅地停止并发”是两件事,前者入门就能体会到,后者才是Go进阶的真正门槛。
4. 从零到一:快速上手的实操路径
4.1 环境搭建与工程模式
学习Go的第一步是搭环境,这个过程没什么难度。到官网下载对应系统的安装包,安装完成后在终端输入go version,能正常输出版本号就算成功。需要了解几个环境变量:GOROOT是Go的安装目录,GOPATH是工作区目录,GOBIN是编译后的二进制安装目录。不过现在的Go早已切换到模块化管理模式,你不需要手动把这些目录维护得井井有条。
我的建议是每个新项目都用标准的module模式起步。建一个项目目录,在里面执行:
go mod init 你的模块名这条命令会生成一个go.mod文件,之后你引入的所有第三方依赖都会自动记录在里面。这样项目之间互不干扰,也不需要把代码强行塞到某个固定目录,用起来非常清爽。还有一个隐藏福利:因为Go的工具链是统一的,go fmt负责格式化代码,go vet负责静态检查,go doc可以查标准库文档,这些命令从第一天就能帮你养成好习惯。
搭完环境不要急着学框架,先解决一个小问题:把一段输入字符串里的数字提取出来并计算总和。用标准库的字符串处理和正则,看看你能不能用最少的代码完成。
4.2 基础语法速览
Go的语法紧凑,但核心概念并不少。我整理了一个能覆盖大部分日常编码的示例,建议你亲手敲一遍:
package main import ( "fmt" "strconv" ) type User struct { Name string Age int } func (u User) say() string { return u.Name + " 今年 " + strconv.Itoa(u.Age) + " 岁" } func findUser(id int) (User, error) { if id <= 0 { return User{}, fmt.Errorf("用户ID不合法") } return User{Name: "某用户", Age: 18}, nil } func main() { u, err := findUser(1) if err != nil { fmt.Println(err) return } defer fmt.Println("main 函数即将结束") fmt.Println(u.say()) }在这个示例里要关注几个关键点:函数可以返回多个值,所以Go的错误处理非常显式,(User, error)是常见写法,不要忽略error返回值;结构体上可以定义方法,func (u User) say()这种写法叫接收器;defer会把后面的语句延迟到函数返回前执行,常用来释放资源。
Go也有接口,而且是隐式实现。你不需要显式声明某个结构体实现了哪个接口,只要它的方法集合满足接口要求就行。这个设计让代码的解耦变得非常灵活,理解它之后你会更明白为什么Go社区鼓励面向接口编程。
4.3 第一个并发Demo:并发下载器
学完基础语法就可以动手写第一个真正有用的并发程序。我推荐“并发下载器”这个练手项目,因为它刚好把你学的goroutine、channel、WaitGroup、信号量限流全部串起来。
package main import ( "fmt" "sync" "time" ) func download(name string) { fmt.Printf("开始下载 %s\n", name) time.Sleep(200 * time.Millisecond) fmt.Printf("完成下载 %s\n", name) } func main() { urls := []string{"文件A", "文件B", "文件C", "文件D", "文件E", "文件F", "文件G", "文件H"} const maxConcurrent = 3 sem := make(chan struct{}, maxConcurrent) var wg sync.WaitGroup for _, u := range urls { wg.Add(1) go func(name string) { defer wg.Done() sem <- struct{}{} // 获取信号量,超出并发数时阻塞 download(name) <-sem // 释放信号量 }(u) } wg.Wait() fmt.Println("全部下载完成") }这里sem是一个容量为3的带缓冲channel,用它来限制同时下载的文件数。每个goroutine启动后先尝试往sem里投一个空结构体,如果缓冲区满了就等待,下载完成后再把位置释放出来。struct{}不占用内存,是Go里做“信号”的常用手段。
运行这个程序,你会看到下载操作始终只以3个为一组推进。如果你之前只用脚本语言写过代码,第一次体验这种“用同步逻辑轻松控制并发”的丝滑感,印象会非常深刻。
4.4 推荐的学习节奏与练手项目
我给一个参考节奏,它来自很多人的共同经验,也适合大多数自学者:
| 阶段 | 时长 | 目标 | 练习项目 |
|---|---|---|---|
| 语法入门 | 1周 | 掌握变量、函数、结构体、接口、错误处理 | 完成命令行待办清单 |
| 并发基础 | 1周 | 理解goroutine、channel、WaitGroup | 并发下载器、并发计数器 |
| 工程化 | 1周 | 掌握go mod、标准库Web服务、日志和测试 | 写一个带接口的短链接服务 |
| 综合实战 | 1-2周 | 综合运用并发与Web开发 | 内存版KV存储或迷你聊天室 |
不要贪快,关键是每个阶段都要有能跑起来的代码。看十篇教程不如自己写一次,这句话在Go这里极其适用。Go的语法太简单,容易让人产生“一看就会”的错觉,真正动手写的时候才能暴露理解上的漏洞。
5. 常见误区与避坑经验实录
5.1 Buzzword一:Go语法太简单,没有技术含量
这是我在各种技术讨论里见过最多的说法。但这其实是一个彻头彻尾的误解:简单是Go刻意设计的目标,绝不等于弱小。真正的难点从来不是语法层面,而是你能否在真实的高并发场景里把goroutine用对、把channel的生命周期管好、把内存和GC的开销控制住。这些难度分散在学习者的视野盲区里,等你在线上看到一个goroutine泄漏导致的内存增长曲线时,才会明白“简单语法”背后的复杂性。
下围棋的规则也很简单,不到十句话就能讲完,但你肯定不能说围棋没有技术含量。规则简单意味着入门门槛低,而策略复杂度才是决定天花板高度的东西。Go就是这样一门语言,它把那些和业务无关的复杂语法全部砍掉,让你把脑力集中在真正重要的问题上:架构设计、数据一致性、性能调优。
5.2 Buzzword二:学了Go只能做后端,没前途
后端本身就是就业市场中盘子非常大的方向,Go在后端领域的机会已经足够多,所以“只能做后端”这个说法并不准确。更真实的情况是Go还大量出现在云原生基础设施、命令行工具、网络代理、数据管道、嵌入式网关这些领域。随着物联网和边缘计算的发展,Go交叉编译和低资源占用的优势会让它出现在越来越多的场景里。
当然,我也不主张把话说绝对。Go不是万能银弹,某些以快速迭代为主的业务项目用动态语言确实更快,某些需要极致底层控制的系统软件用C++或Rust更合适。但这不是“没前途”的理由,而是说明你要学会在合适的场景选合适的语言。
5.3 Buzzword三:并发就是无脑加go关键字
这是新手最危险的理解方式。go关键字只是创建了一个并发体,真正的工程难度在于:这些goroutine何时退出?它们之间的数据如何同步?一个goroutine崩溃了会不会拖垮整个进程?这些问题的答案不会因为加了一个关键字就自动正确。
我在实际项目中见过三种典型事故:goroutine泄漏,某个等待channel的协程永远不退出,数量越积越多最后内存爆炸;数据竞争,多个goroutine同时写同一个变量或map,程序偶发panic;死锁,两个channel互相等待,程序直接卡死。好消息是Go提供了很好的工具来帮你排查:用go run -race可以检测数据竞争,用net/http/pprof可以看到当前所有goroutine的调用栈,用go test配合超时控制可以锁定死锁位置。
这些都是解决并发问题的标准手段,但前提是你得知道并发不是只写关键字的游戏。
5.4 我踩过的三个实际坑
直接分享三个我印象最深的线上问题,希望你能绕开。
第一个是goroutine泄漏。一个消息推送服务每天要处理百万级消息,某次上线后内存像开闸一样涨。排查时我用go tool pprof抓了goroutine数量,发现大量goroutine阻塞在一个channel上,原因是某个下游服务的响应处理分支忘记调用关闭channel,导致所有等待的协程永远挂起。修复方式是统一约束:谁创建channel谁负责关闭,并且在所有消费路径上使用带超时的select。
第二个是并发map写入panic。一个统计服务里有多个goroutine同时往一个全局map写数据,平时测试没问题,流量一大就偶发报错“concurrent map writes”。原因就是Go的map不是并发安全的。解决方法是给map加读写锁,或者改用sync.Map,更根本的是从设计上尽量减少共享状态。
第三个是循环里的defer。我写一个批处理函数时,在循环中用了defer file.Close(),结果所有文件句柄都堆积到函数返回时才释放,瞬间打开了几千个文件。对这个问题,正确的做法是把循环体内的逻辑包到一个单独的函数里,或者不用defer直接显式关闭。这是个非常基础的资源管理问题,但恰恰是初学者最容易踩的。
我把这些问题整理成一张速查表,方便你以后对照:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| goroutine数量只增不减 | channel未关闭造成永久阻塞 | pprof抓取goroutine栈 |
| map并发写导致panic | 全局map无锁保护 | go run -race检测数据竞争 |
| 循环内defer导致句柄堆积 | defer延迟到函数返回才执行 | 包一层函数或显式关闭 |
| channel重复close panic | 多处控制关闭逻辑 | 统一由生产者close |
| 程序卡死无输出 | channel死锁 | 检查收发双方是否匹配 |
6. 关于要不要学Go,我的几点真实建议
说了这么多,回到最初的问题:你到底该不该学Go?我的判断很明确:如果你是后端开发、运维或基础设施方向的从业者,非常值得。Go的学习曲线在所有编译型语言里属于最平缓的一类,你不需要会C++也能直接上手,而且它的应用范围确实在逐年扩大。
我更想说的是学习方法。Go语言的语法学完只需几天,真正难的是把并发的思维用对地方。不要满足于会跑教程里的Demo,试着用Go重写一个你以前用其他语言做过的小项目,你可以体会语言之间的思维差异。读一读标准库的源码,你会发现自己写的代码和官方代码之间的差距,这也是进阶最快的捷径。
我看过太多人收藏一堆教程,却从没有完整写过一个Web服务。按我自己的体验,最有效的方式是:第一天搭环境,第二周写一个能跑的HTTP接口,第三周给它加上并发处理和中间件,一个月后你已经能自信地说自己会用Go了。等你在线上环境因为goroutine泄漏睡过几觉之后,你对并发的理解一定会远超那些只是“看过文档”的同行。这就是我写这篇文章最想传达的东西:Go是一个值得投入的方向,但它真正有价值的部分,是需要你动手写坏了再改好才能学到的。