这周我额外学了一门 Go,把它当作后端岗的加分技能。Java 和 Go 看着都是写业务逻辑的语言,真上手才发现差别大得离谱:没有 class、没有 public/private 关键字、循环只剩一个 for。这篇是我第一周的学习复盘,把从环境到写出第一个小项目的过程用最直白的话讲一遍,给同样想给简历加点料的 Java 后端同学避避坑。
一、先把"厨房"搭起来:GOPATH 和 go mod 到底差在哪
刚装好 Go,教程就让我敲go version验证。能打印出go version go1.x.x就说明装好了。然后一堆文章开始讲 GOROOT、GOPATH、PATH,我一开始很懵。
后来我把自己代入进去了:GOROOT 就像 Go 自带的"标准厨房",编译器、标准库都在里面,装语言时自动放好,平时不用管。PATH 就是让终端"认识 go 这个命令"的设置,装完一般自动配好。最坑的是 GOPATH。
GOPATH 是早期 Go 的"公共大仓库"模式:你写的所有代码、所有第三方库,全塞进同一个全局目录。结果就是项目 A 和项目 B 的依赖互相打架,版本冲突到崩溃。
Go 1.11 之后进了 Module 时代,现在每个项目自带一个go.mod当"私人小仓库",依赖版本锁得死死的,项目之间互不污染。这就是为什么现在写 Go 基本不用管 GOPATH 了,go mod 才是日常。
这张图说的是:老办法把所有菜放一个冰箱会串味,新办法每人一个独立冰箱,干净。
新建项目就是一句话:
go mod init bookcli它会生成go.mod文件,以后你 import 的第三方库版本都记在里面。
二、package main 和"大写小写":Go 的入口和权限很特别
第一个程序长这样:
packagemainimport"fmt"funcmain(){fmt.Println("Hello Go")}这里有两个 Java 里没有的概念,我得单独拎出来说。
第一,package main 是程序的"开机键"。普通 package 像"零件箱",别人 import 进来拿零件用;而 package main 是"带总开关的机器",只有它里面有 func main(),你 go run 时 Go 才知道从哪启动。没有 main 的包只是库,自己跑不起来。
第二,Go 没有 public/private 关键字,靠首字母大小写决定能不能被外面访问。这个设计第一次见真的反直觉。
- 首字母大写 = 导出(相当于 public),包外能访问
- 首字母小写 = 包内私有(相当于 private),只有同包内能用
比如结构体字段Name string能被别的包读到,name string就只能自己包里用。记住,这不是风格,是语法规则。我写第一个项目就因为字段名小写,别的包死活读不到,查了半天才反应过来。
三、变量声明三兄弟:var、:=、const,谁该什么时候用
Java 里我只有一种写法int x = 10;。Go 给了我三种,而且语义完全不同,这是面试官爱挖的点。
生活化理解:var 像"先挂牌子再放货",可以只声明不赋值;:= 像"边放货边自动贴标签",Go 看 10 是 int 就标成 int,但只能在函数里用;const 像"焊死的水泥墩",编译期就定死,运行期谁也改不了。
| 写法 | 能用在哪里 | 要不要立刻赋值 | 典型用途 |
|---|---|---|---|
var x int | 包级和函数内都行 | 不用,可后置 | 包级变量、先声明后赋值 |
x := 10 | 只能在函数里 | 必须,且自动推导类型 | 函数内临时变量 |
const PI = 3.14 | 包级和函数内 | 必须 | 配置项、枚举值 |
还有两个第一天没讲但第二天很重要的事。
零值机制:Java 里局部变量不初始化就用会报错,Go 不一样,变量声明了就有默认值。数值是 0,字符串是空串,布尔是 false,指针/切片/map 是 nil。这比 Java 友好,不会出现"随机垃圾值"。
iota 枚举:Java 有 enum,Go 没有这个关键字,靠const + iota模拟。iota 是编译器在 const 块里自动从 0 递增的计数器:
const(Mon=iota+1// 1Tue// 2Wed// 3)强类型:Go 比 Java 还狠,完全没有隐式转换。哪怕都是数字,int 和 int32 是两种类型,必须显式写int32(x)。刚开始写别扭,但能挡掉一堆隐蔽 bug。处理中文更要小心,字符串底层是 UTF-8 字节,中文占 3 字节,遍历中文要用 rune 或 range,用 []byte 会切开汉字乱码。
四、切片:Go 用得最多的数据结构,也是第一个大坑
数组我 quickly 过了一下:定长、值类型(赋值/传参是整个拷贝)、长度还参与类型([5]int和[10]int是两种类型)。实际开发几乎不用数组,真正天天用的是切片 slice。
切片是数组的"动态视图",底层就三个字段:指向底层数组的指针 ptr、当前长度 len、容量 cap。它是引用类型,多个切片可以指向同一块底层数组。
第一个坑:append 扩容。往切片加元素用 append。当 len 等于 cap(底层数组满了),append 会分配一块更大的新数组(通常翻倍)、拷贝旧数据、写入新元素、让切片指向新数组。所以一定要用s = append(s, x)把返回值接回来,不接的话,如果发生了扩容,你改的是旧数组,新数据丢了。
第二个坑更隐蔽:切片截取共享底层数组。s[1:3]这种截取不会复制数据,而是生成一个新切片和原切片共享同一块底层数组。你改了截取出来的切片,原数组也跟着变。还有个隐藏内存坑:从超大底层数组里截取一小片长期持有,这一小片会拖住整个大数组无法被 GC 回收。解决方法是s2 = append([]int{}, s1...)强制拷贝一份。
这张图说的是:两个切片看着独立,其实共用一个冰箱,动一个另一个也变。
我做的那个练手项目(几万台设备、温度振动数据,下面简称平台)里,切片是主力。一次从缓存批量拉回几万台设备的温度读数,直接塞进 []float64,动态扩容让我不用预先知道设备数量。但截取共享底层数组的坑真实存在:从全天数据大切片里截取一小段做实时展示并长期缓存,这一小段会拖住全天数组不被回收,内存悄悄涨。实战里要么拷贝,要么用 copy() 显式拷贝。
| 对比项 | 数组 array | 切片 slice |
|---|---|---|
| 长度 | 固定,写死 | 动态,能长能短 |
| 类型 | [5]int和[10]int是不同类型 | []int就一种 |
| 传参行为 | 值类型,整体拷贝 | 引用类型,共享底层数组 |
| 日常使用 | 几乎不用 | 天天用 |
五、map:nil map 写入直接崩,这个坑我记一辈子
map 就是键值对抽屉柜,按标签直接拿东西。三个核心特性:无序、引用类型、nil map 不能写。
最易错的是 nil map 和空 map 的区别:
var m map[string]int是 nil,读可以(返回零值),写必 panic。m := make(map[string]int)或m := map[string]int{}是空但可用。结论:凡要写 map,先 make 或给字面量,别裸声明。
map 的 key 必须可比较(能做 ==),所以 int/string/bool/可比较结构体可以当 key,slice/map/func 不行。还有个设计:Go 故意让 map 遍历顺序随机,防止你写代码时"以为有序"。需要有序就自己把 key 取出来排序。
查一个 key 在不在,用v, ok := m[k]的 ok 模式,别把零值误当"有值"。
平台里,map 是缓存主力。用map[string]float64(key 设备ID,value 最新温度)做设备最新状态缓存,O(1) 查某个设备当前温度。nil map 的坑真实存在:函数返回了一个没 make 的 map 然后别人往里写,直接线上崩溃。团队规范是"函数内要返回的 map 先 make"。
六、函数与 defer:Go 的"身份证"和"自动收尾"
Go 和 Java 最大的差异之一,是函数可以返回多个值。最经典的就是 (result, error):
funcdiv(a,bint)(int,error){ifb==0{return0,errors.New("除数不能为0")}returna/b,nil}正常返回v, nil,出错返回0, err,调用方用v, err := f()且必查 err。这是 Go "错误即值"哲学的落地,比 Java 抛异常更可控。
控制结构也有意思:Go 只有 for 一个循环,没有 while。用for cond {}当 while,for {}当死循环。switch-case 执行完自动 break,不用手写防穿透。
最让我印象深的是 defer,面试必考。defer 把语句推迟到函数返回前执行,多个 defer 按栈的顺序,后进先出:
这张图说的是:defer 像叠盘子,最后叠的先拿。典型用途是资源释放,打开文件/连接/加锁后立刻 defer close(),保证函数退出时一定释放,不会漏。
还有 init 和 main 的顺序:包级变量初始化 → 各包 init() → main 包 init() → main()。init 常做配置加载、连接池初始化这些"启动前必须准备好"的事。
七、Day6 实战:不看教程写一个图书管理 CLI
前面五天都是"听讲解跟敲",第六天反过来:合上教程,从零写个图书管理命令行小程序。要求用 go mod init 起项目,定义书的结构体,用切片或 map 存书,做个循环菜单实现增删改查。
这是我第一次不查资料独立写,卡了几次:比如删除时忘了处理"id 不存在",查不到时返回零值没提示。写完跑起来把每个功能点一遍,才算过关。这个 cli 里我把前五天学的都用上了:结构体、var/:=、slice、for/switch、多返回值思路。
下面是我写的简化版核心(脱敏,只留结构):
packagemainimport"fmt"typeBookstruct{IDintTitlestringAuthorstringPricefloat64}funcmain(){books:=make(map[int]Book)// 用 map 按 id 查得快// 循环打印菜单,输入数字选择,直到选退出// 1 添加 2 删除 3 修改 4 查询 5 列出全部 6 退出_=books fmt.Println("图书管理 CLI 跑通了")}写完这版,我对"切片增删改查 + map 按 key 查 + for 循环菜单"这套组合拳才算真正内化了。
Java 后端加学 Go 的速查表
| Java 习惯 | Go 对应写法 | 注意点 |
|---|---|---|
| class + 方法 | struct + 独立函数 | Go 没有类 |
| public / private | 首字母大小写 | 大写才算导出 |
| try-catch | 多返回值 (T, error) | 必须查 err |
| while 循环 | for cond {} | 只有 for |
| HashMap | map[K]V | nil 不能写 |
| 自动类型转换 | 完全没有 | 必须显式写 |
这周的整体感受
第一周最大的冲击是:Go 把 Java 里很多"写起来啰嗦但明确"的东西砍掉了,换成了更笨拙但更安全的规则。没有 class 我用 struct 加函数;没有 public/private 我用大小写;没有 try-catch 我用多返回值 (result, err)。初写别扭,但写顺了发现它逼你写出更干净的代码。
切片和 map 的两个坑(截取共享底层数组、nil map 写入 panic)是这周最值钱的经验,光看教程记不住,自己写项目踩一遍才刻进脑子。
下周进第 2 周,开始学 struct 的方法、接口 interface,以及 Go 的错误处理惯用法。接口那块据说和 Java 的接口完全不同,已经有点期待踩坑了。