大家好,我是极客老墨!
从 Java 或 Python 转 Go 的时候,我在变量声明这块卡了好一会儿。
不是说 Go 的变量有多复杂,而是它给了你好几种声明方式。var 和 := 到底啥区别?类型推导怎么玩?指针和 Java 有啥不同?常量还能玩出花来?
这篇文章一次讲透。
变量声明:var 和 :=
Go 有两种主要声明变量的方式。
用 var 声明
这是最"正统"的写法,适合先声明、后赋值的场景:
1// 声明单个变量
2var name string
3name = "Go"
4
5// 声明时直接赋值(类型可省略)
6var age = 18 // Go 自动推导为 int
7
8// 批量声明
9var (
10 x int
11 y float64
12 z bool
13 s string
14)
用 := 声明
这是 Go 的语法糖,声明 + 赋值一步到位:
1// 必须初始化,Go 自动推导类型
2message := "Hello, Go!"
3fmt.Println(message)
4
5// 接收函数返回值
6result, err := someFunc()
什么时候用哪个?
用 var 的场景:
- 先声明后赋值
- 全局变量
- 需要指定类型
用 := 的场景:
- 局部变量
- 声明 + 赋值同时进行
- 接收函数返回值
⚠️ 重要::= 是声明,不是赋值。已声明的变量不能再用 :=:
1var name = "Go"
2name := "Python" // ❌ 编译错误:no new variables
3name = "Python" // ✅ 正确:赋值
基本数据类型
Go 的基本数据类型分几大类。
整数类型
1// 有符号整数
2var i1 int8 // 范围:-128 ~ 127
3var i2 int16 // 范围:-32768 ~ 32767
4var i3 int32 // 范围:-21亿 ~ 21亿
5var i4 int64 // 范围:很大
6
7// 无符号整数(只能存非负数)
8var u1 uint8 // 范围:0 ~ 255
9var u2 uint16 // 范围:0 ~ 65535
10var u3 uint32
11var u4 uint64
12
13// 平台相关(推荐日常使用)
14var i int // 32 或 64 位,取决于系统
15var u uint // 同上
16
17// 特殊类型
18var b byte // uint8 的别名,存单个字节
19var r rune // int32 的别名,存 Unicode 字符
选择建议:日常用 int 或 int64,需要精确控制内存时用具体类型。
浮点数类型
1var f1 float32 // 精度约 7 位小数
2var f2 float64 // 精度约 15 位小数(推荐)
3
4// 浮点数比较要小心
5a := 0.1 + 0.2
6b := 0.3
7fmt.Println(a == b) // ❌ false!精度问题
8
9// 正确做法:比较差值
10diff := a - b
11if math.Abs(diff) < 0.0001 {
12 fmt.Println("近似相等")
13}
布尔类型
1var flag bool // 默认 false
2flag = true
3
4// 逻辑运算
5fmt.Println(true && false) // false(与)
6fmt.Println(true || false) // true(或)
7fmt.Println(!true) // false(非)
字符串类型
1var s1 string // 默认 ""
2s1 = "Hello"
3
4// 字符串是不可变的
5s1[0] = 'h' // ❌ 编译错误
6
7// 获取长度(字节数)
8fmt.Println(len(s1)) // 5
9
10// 获取字符数(Unicode)
11fmt.Println(len([]rune(s1))) // 5
12
13// 字符串拼接
14s2 := "Go"
15s3 := s2 + " is great"
16s3 += "!" // 简写形式
byte 和 rune
这两个是 Go 处理字符的关键:
1// byte 是 uint8 的别名,存单个字节
2var b byte = 'A' // ASCII 字符
3fmt.Println(b) // 65
4
5// rune 是 int32 的别名,存 Unicode 字符
6var r rune = '中' // Unicode 字符
7fmt.Println(r) // 20013
8
9// 遍历字符串
10s := "Hello, 世界"
11for i, ch := range s {
12 // ch 是 rune 类型
13 fmt.Printf("%d: %c (%d)\n", i, ch, ch)
14}
要点:
byte存 ASCII(1 字节)rune存 Unicode(4 字节)- 遍历字符串时用
range,自动按字符(rune)遍历
类型推导
Go 会根据初始值自动推导类型:
1// 整数 → int
2var a = 10
3fmt.Printf("Type: %T\n", a) // int
4
5// 浮点数 → float64
6var b = 3.14
7fmt.Printf("Type: %T\n", b) // float64
8
9// 字符串 → string
10var c = "Go"
11fmt.Printf("Type: %T\n", c) // string
12
13// 多变量推导
14x, y := 10, 20
15fmt.Printf("x: %T, y: %T\n", x, y) // x: int, y: int
16
17// 函数返回值推导
18name := getName() // 函数返回 string,name 就是 string
注意::= 只能用于新变量,已声明的变量不能重新推导类型。
常量与 iota
常量用 const 声明,编译期确定值:
1const PI = 3.14159
2const MaxAge = 120
3
4// 批量声明
5const (
6 StatusOK = 200
7 StatusNotFound = 404
8 StatusServerError = 500
9)
iota:枚举生成器
iota 是常量组的行索引,从 0 开始:
1const (
2 a = iota // 0
3 b = iota // 1
4 c = iota // 2
5)
6
7// 简写(同一行可以省略)
8const (
9 d = iota // 0
10 e // 1(自动继承上一行的 iota)
11 f // 2
12)
13
14// 跳值用法
15const (
16 _ = iota // 0(跳过)
17 KB = 1 << (10 * iota) // 1 << 10 = 1024
18 MB // 1 << 20
19 GB // 1 << 30
20)
21
22// 混合使用
23const (
24 FlagRead = 1 << iota // 1
25 FlagWrite // 2
26 FlagExecute // 4
27)
iota 常见用法:
- 生成枚举值
- 生成位掩码
- 生成常量序列
Go 中的常量很重要,也很“与众不同”,这里只是混个眼熟,下一篇我们还要重点介绍它。
指针
Go 有指针,但不像 C 那样危险。
基本用法
1// & 取地址
2x := 10
3p := &x // p 是 *int 类型,存 x 的地址
4
5// * 解引用
6fmt.Println(*p) // 10(读取 p 指向的值)
7*p = 20 // 修改 p 指向的值
8fmt.Println(x) // 20(x 也变了)
指针作为函数参数
1func swap(a, b *int) {
2 // 交换指针指向的值
3 *a, *b = *b, *a
4}
5
6func main() {
7 x, y := 10, 20
8 swap(&x, &y)
9 fmt.Println(x, y) // 20 10
10}
new 函数
1// new 创建指针并初始化为零值
2p := new(int) // *int,值为 0
3*p = 100
4fmt.Println(*p) // 100
要点:
- 指针存的是内存地址
&取地址,*解引用- 指针传递可以实现"引用传递"效果
- Go 有垃圾回收,不用担心内存释放
变量的作用域
Go 的变量有作用域,块级作用域:
1var x = "全局"
2
3func main() {
4 var x = "局部"
5 fmt.Println(x) // 局部(局部变量遮蔽全局变量)
6
7 // 内层块
8 {
9 var x = "内层"
10 fmt.Println(x) // 内层
11 }
12
13 fmt.Println(x) // 局部
14}
15
16func other() {
17 fmt.Println(x) // 全局(访问不到 main 的局部变量)
18}
作用域规则:
- 大括号
{}定义新的作用域 - 变量遮蔽 (Variable Shadowing):内层作用域可以定义与外层同名的变量,此时内层变量会“遮蔽”外层变量。
- 循环变量作用域:在 Go 1.22+ 中,
for循环的变量在每次迭代中都是新变量。
匿名变量:_
_ 是匿名变量,用来忽略不需要的返回值:
1func getUser() (name string, age int, email string) {
2 return "Alice", 30, "alice@example.com"
3}
4
5func main() {
6 // 只需要 name
7 name, _, _ := getUser()
8 fmt.Println(name)
9
10 // 只需要 email
11 _, _, email := getUser()
12 fmt.Println(email)
13}
注意:_ 不能读取值,赋值给它会丢弃。
完整示例
把上面的知识点串起来:
1package main
2
3import "fmt"
4
5const (
6 StatusOK = iota
7 StatusPending
8 StatusFailed
9)
10
11func swap(a, b int) (int, int) {
12 return b, a
13}
14
15func main() {
16 // 变量声明
17 var name string = "Go"
18 version := 1.21
19
20 // 指针
21 ptr := &version
22 fmt.Printf("Value: %d, Pointer: %d\n", version, *ptr)
23
24 // 常量
25 fmt.Printf("Status: %d\n", StatusOK)
26
27 // 多重赋值和交换
28 x, y := 10, 20
29 x, y = swap(x, y)
30 fmt.Printf("After swap: x=%d, y=%d\n", x, y)
31
32 // 匿名变量
33 _, age := "Bob", 25
34 fmt.Printf("Age: %d\n", age)
35}
输出:
Value: 1, Pointer: 824633794576
Status: 0
After swap: x=20, y=10
Age: 25
老墨踩过的坑
说变量会玩“躲猫猫”,可不是跟你开玩笑的。下面这几个坑,老墨可是用真金白银的 Bug 换来的教训。
坑 1:幽灵般的“影子变量” (Shadowing)
这是 Go 最隐蔽的坑,没有之一。看这段代码:
1var user = "Guest"
2
3func main() {
4 isVip := true
5 if isVip {
6 // ❌ 坑在这里!用 := 声明了一个新的局部 user
7 user, err := getUserName()
8 fmt.Println("内部用户:", user) // 打印 VipUser
9 }
10 fmt.Println("外部用户:", user) // 打印 Guest!外面的变量没变
11}
虽然在 if 块里你觉得改了 user,但因为用了 :=,Go 偷偷在 if 的作用域里创建了一个同名变量,把外面的变量“遮住”了。最常见也最危险的场景是在处理 err 时:
1func main() {
2 n, err := f1()
3 if err != nil { return }
4
5 if n > 0 {
6 // ❌ 这里的 n 和 err 都是新声明的局部变量!
7 n, err := f2()
8 if err != nil { return }
9 fmt.Println("内部 n:", n)
10 }
11 // ⚠️ 这里的 n 依然是 f1 返回的值,而不是 f2 的
12 fmt.Println("外部 n:", n)
13}
避坑指北:
- 手动补救:想改外部变量,请先用
var err error声明,然后使用=进行赋值,而不是:=。 - 工具揪影子:安装
shadow工具并集成到 lint 中(go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest),或者使用go vet加载该插件。
坑 2:类型系统的“洁癖”
别的语言可能帮你偷偷转了,Go 绝对不惯着你。
1var a int = 10
2var b int64 = 20
3// c := a + b // ❌ 编译报错:invalid operation (mismatched types int and int64)
哪怕你觉得 int 在 64 位系统下就是 int64,编译器也会坚持认为它们是两个物种。
避坑指北:
- 涉及不同类型计算,必须显式强转:
c := int64(a) + b。 - 常量(无类型)混用没问题,变量绝对不行。
坑 3:空指针的“自作多情”
声明了指针就开始用,是新手最容易犯的错。
1var p *int
2// *p = 100 // ❌ 运行时 Panic: invalid memory address or nil pointer dereference
指针默认是 nil,它还没指向任何地方呢,你就想往里面塞值?
避坑指北:
- 用指针前,先用
new()或&初始化。 - 永远记得检查
if ptr != nil。
老墨总结
Go 变量和常量的 5 个关键点:
- 声明方式:
var适合先声明后赋值,:=适合声明+赋值一步到位 - 基本类型:int 系列、float64、bool、string、byte、rune
- 指针:
&取地址,*解引用,传递指针可以实现引用传递 - 常量:
const声明,iota生成枚举值 - 作用域:块级作用域,内层遮蔽外层
实战建议:
- 日常用
int、float64、string,不用纠结具体类型 - 指针传递比值传递更高效,但要注意 nil 检查
- 常量组用
iota生成枚举,清晰又安全 - 多返回值用
_忽略不需要的值 - 变量命名用驼峰,
const用全大写加下划线
你在变量声明上踩过什么坑?指针和 Java 比有什么不同的感觉?欢迎评论区聊聊!
练习题
- 编写程序,交换两个整数的值(用函数和不用函数两种方式)
- 定义常量组,使用 iota 生成周一到周日的枚举值
- 写一个函数,接收字符串,返回字节切片和字符切片
- 理解以下代码的输出结果:
1x := 10 2p := &x 3*p = 20 4fmt.Println(x) - 创建一个程序,演示不同整数类型的取值范围
思考题
- 为什么 Go 要设计
:=这种语法糖? 它解决了什么痛点? - byte 和 rune 有什么区别? 什么场景下必须用 rune?
- 指针传递和值传递怎么选? 什么情况下用指针更高效?
- iota 的行索引是怎么计算的? 为什么有时候需要用
_跳过?
评论区秀出你的想法,大家一起讨论!
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
本文示例代码在 go-tutorial-code/03-var。