第一章:初识——"这语言有点东西啊!"
每个后端程序员都听说过Go的传说:C的性能,Python的开发效率。
当你第一次写出Hello World的时候,内心是激动的:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}简洁!优雅!没有分号!(虽然其实是编译器帮你加的)
你开始觉得Go就是后端的未来。Docker是Go写的,Kubernetes是Go写的,连以太坊都是Go写的。
这时候的你,对Go充满了憧憬,觉得自己马上就要站在云原生的风口上起飞了。
第二章:热恋——"语法真简单!"
2.1 变量声明:两种方式,随便选
// 方式一:var声明
var a = "123"
var a0 int // 默认值为0,不用写分号!
// 方式二:短声明(推荐)
b, c := "4", "5"你心想:这也太简单了吧!比Java不知道简洁到哪里去了。
然后你发现了Go的第一个"特色":
// 定义了变量不用?编译不通过!
var unused string // 编译器:你搁这浪费内存呢?Go编译器比你妈管得还严。变量定义了不用?不行!import了包不用?不行!
这时候你开始怀疑:这到底是在写代码还是在接受思想教育?
2.2 数据类型:似曾相识
// 基础类型,和Java差不多
bool, byte, int8, int16, int32, int64, float32, float64
// 但是多了这些
rune // int32的别名,表示Unicode码点
uintptr // 存指针的,C语言DNA动了
complex // 复数,数学系狂喜2.3 控制流程:只有for,没有while
// 普通for循环
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// 这是while
for x < 10 {
x++
}
// 这是while(true)
for {
break
}Go设计者:while是什么?能吃吗?我们只要for!
你:好吧,反正也能实现...
第三章:磨合期——"等等,这语法有点怪啊"
3.1 类型声明在后面???
func add(x, y int) int {
return x + y
}Java程序员:int add(int x, int y) 不香吗?
Go设计者:不香。我们要从左往右读,这样更符合人类语言习惯。
你:???人类语言是"整数加法接受整数x和整数y返回整数"???
3.2 数组声明更是重量级
// 第一次看到这个语法,我整个人都不好了
s := []int{1, 2, 3, 4}
// 定义一个3个元素的数组
var arr [3]int
// 切片(动态数组)
slice := make([]int, 5, 10) // 长度5,容量10方括号在变量名后面!类型也在后面!整个世界都颠倒了!
3.3 多返回值:甜蜜的负担
// 除法函数,返回结果和错误
func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("divide by zero")
}
return x / y, nil
}
// 调用
result, err := div(10, 0)
if err != nil {
// 处理错误
}多返回值确实很方便,但你很快就会发现...
第四章:争吵——"if err != nil 写到吐"
4.1 错误处理:Go的噩梦
f, err := os.Open("/opt/file/test.txt")
if err != nil {
return err
}
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}一个函数里一半代码都在处理错误!
Java程序员:我们有try-catch啊...
Go设计者:try-catch会导致滥用,显式错误处理更清晰。
你看着屏幕上密密麻麻的if err != nil,开始怀疑人生。
据统计,一个中型Go项目中,if err != nil出现的次数可以绕地球两圈。
4.2 panic-recover:更难用的try-catch
func testPanic() {
defer func() {
if err := recover(); err != nil {
debug.PrintStack()
}
}()
panic("holy shit")
// 下面这句执行不到了
fmt.Println("test shit")
}你:这不就是try-catch吗?
Go设计者:不一样,这个更...呃...更Go。
你:???
4.3 没有三元运算符
// Java/JS: a = (age >= 7) ? "小学生" : "学龄前"
// Go: 不好意思,没有这个语法
var a string
if age >= 7 {
a = "小学生"
} else {
a = "学龄前"
}Go设计者认为三元运算符会降低代码可读性。
你:四行代码变一行怎么就降低可读性了???
第五章:冷战——"为什么要保留这些东西"
5.1 struct:Java程序员的困惑
type Member struct {
name string
age int
idNo string
mobileNo string
registerDate string
}
zhangsan := Member{"Zhang San", 20, "310xxx", "+86-111", "2022-03-15"}你:为什么不直接用class?struct不是C语言的东西吗?
Go设计者:我们没有class,只有struct。Go追求简单。
你:那继承呢?
Go设计者:没有继承,用组合。
你:多态呢?
Go设计者:用interface。
你:那封装呢?
Go设计者:首字母大写就是public,小写就是private。
你:......这也太简单了吧?
Go设计者:简单就是美。
5.2 指针:C语言的回忆杀
func testPointer() *int {
a := 10
return &a // 返回局部变量的地址,在C里这是作死
}
// 指针操作
p := &a // 取地址
*p++ // 通过指针修改值C程序员:终于有人理解我了!
Java程序员:我以为我这辈子都不用再碰指针了...
Go设计者:指针是好东西,但我们不让你做指针运算,保证你不会搞出野指针。
你:那为什么不干脆全用引用?
Go设计者:(已读不回)
5.3 大小写决定访问权限
type User struct {
Name string // 首字母大写,public
age int // 首字母小写,private
}你:所以我想把一个字段从private改成public,就要改名字?
Go设计者:是的。
你:那IDE的重构功能...
Go设计者:反正重构方便,有什么问题吗?
第六章:和解——"真香定律"
吐槽归吐槽,用久了还是会发现Go的好。
6.1 goroutine:并发从未如此简单
// 启动一个协程,就这么简单
go func() {
fmt.Println("I'm running in a goroutine!")
}()
// 比起Java的线程池...你懂的一个go关键字,轻量级协程就启动了。不用线程池,不用管理生命周期,Go的调度器帮你搞定一切。
6.2 channel:优雅的协程通信
ch := make(chan int, 10)
// 发送
ch <- 42
// 接收
value := <-ch"Don't communicate by sharing memory; share memory by communicating."
这句话我第一次看没懂,用了channel之后秒懂。
6.3 defer:资源管理的救星
func readFile() error {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // 函数结束时自动关闭,不怕忘记
// 读文件操作...
return nil
}再也不用担心忘记关闭资源了!defer就是你的保姆。
6.4 编译速度:快到飞起
$ time go build main.go
real 0m0.892s一秒不到,编译完成。想想Java项目动辄几分钟的编译时间,这不是快,这是在表演。
6.5 部署:一个二进制文件走天下
# 交叉编译到Linux
GOOS=linux GOARCH=amd64 go build -o app
# 部署:把这一个文件扔到服务器就行
scp app server:/opt/没有依赖地狱,没有JVM版本问题,没有node_modules黑洞。
就一个可执行文件,几MB大小,爱复制到哪就复制到哪。
第七章:放弃——"我选择回到Java的怀抱"
7.1 泛型来得太晚
Go 1.18 才正式支持泛型。在此之前:
// 想写一个通用的Max函数?
func MaxInt(a, b int) int { ... }
func MaxFloat(a, b float64) float64 { ... }
func MaxString(a, b string) string { ... }
// 复制粘贴,复制粘贴...
// 或者用interface{},然后到处类型断言
func Max(a, b interface{}) interface{} {
// 一堆switch type
}Java程序员:2004年就有的东西,你们2022年才加?
7.2 包管理的历史包袱
# 早期:GOPATH地狱
export GOPATH=/home/user/go
# 所有项目都在一个目录下,疯狂
# 后来:Go Modules
go mod init
go mod tidy
# 终于正常了,但早期用户已经被伤害过了7.3 错误处理,永远的痛
// 一个正常的业务函数
func ProcessOrder(orderID string) error {
order, err := getOrder(orderID)
if err != nil {
return fmt.Errorf("get order: %w", err)
}
user, err := getUser(order.UserID)
if err != nil {
return fmt.Errorf("get user: %w", err)
}
inventory, err := checkInventory(order.Items)
if err != nil {
return fmt.Errorf("check inventory: %w", err)
}
payment, err := processPayment(user, order)
if err != nil {
return fmt.Errorf("process payment: %w", err)
}
// ... 还有更多的 if err != nil
return nil
}有效代码和错误处理代码的比例大概是 1:1。
你开始怀念Java的try-catch,怀念异常栈,怀念一切美好的东西。
第八章:归来——"真香,但我选择成年人全都要"
最后,你悟了。
每种语言都有自己的设计哲学:
| 特性 | Go的选择 | 为什么 |
|---|---|---|
| 错误处理 | 显式返回 | 强制你处理每一个错误 |
| 继承 | 组合 | 避免复杂的继承层次 |
| 泛型 | 姗姗来迟 | 宁缺毋滥 |
| 异常 | panic/recover | 只用于真正的异常情况 |
| 语法 | 极简 | 25个关键字,学完就能上手 |
Go不是银弹,但它确实解决了一些问题:
- 需要高并发?goroutine
- 需要简单部署?单二进制文件
- 需要快速编译?Go是最快的之一
- 需要统一代码风格?gofmt,强制格式化,没得商量
尾声
"Go语言很无聊,而这正是它最大的优点。" —— 某Go布道者
你可能会放弃Go,也可能最终爱上Go。
但无论如何,学Go的过程中,你会:
- 重新理解错误处理 —— 原来显式处理错误也有它的道理
- 重新认识并发 —— CSP模型比共享内存更清晰
- 重新思考简单 —— 有时候少即是多
最后送大家一段Go语言的Hello World,作为新旅程的开始:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second)
ch <- "Hello from goroutine!"
}()
fmt.Println("Waiting...")
msg := <-ch
fmt.Println(msg)
}这段代码展示了Go最精华的部分:goroutine和channel。
如果你能理解它,说明你已经入门了。
如果你还不能,说明你还需要再被Go折磨一段时间。
共勉。
本文作者已在Go和Java之间反复横跳N次,目前处于"用Go写中间件,用Java写业务"的状态。
据说这种状态的程序员,头发掉得最快。
附录:Go的25个关键字速查
| 关键字 | 用途 | Java对应 |
|---|---|---|
func | 函数定义 | method |
go | 启动协程 | new Thread() |
chan | 通道 | BlockingQueue |
defer | 延迟执行 | try-finally |
select | 多路复用 | - |
struct | 结构体 | class |
interface | 接口 | interface |
map | 字典 | Map |
range | 遍历 | for-each |
fallthrough | switch穿透 | 默认行为 |
package | 包 | package |
import | 导入 | import |
const | 常量 | final |
var | 变量 | var |
type | 类型定义 | class/typedef |
if/else | 条件 | if/else |
for | 循环 | for/while |
switch/case | 分支 | switch/case |
break/continue | 跳转 | break/continue |
return | 返回 | return |
goto | 跳转 | - |
default | 默认分支 | default |