搜 索

Go语言从入门到放弃

  • 303阅读
  • 2022年04月28日
  • 0评论
首页 / 编程 / 正文

第一章:初识——"这语言有点东西啊!"

每个后端程序员都听说过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的过程中,你会:

  1. 重新理解错误处理 —— 原来显式处理错误也有它的道理
  2. 重新认识并发 —— CSP模型比共享内存更清晰
  3. 重新思考简单 —— 有时候少即是多

最后送大家一段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
fallthroughswitch穿透默认行为
packagepackage
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
评论区
暂无评论
avatar