目录

Go-03 变量常量与基础语法

1. 变量声明

Go 是静态强类型语言,每个变量在编译期都有确定的类型。但 Go 又通过类型推断、短声明等语法糖大幅减少了冗余,写起来接近动态语言。本章把变量、常量、作用域、运算符这些"地基"讲透。

1.1 var 声明的四种写法

package main

import "fmt"

func main() {
	// 1) 完整写法:var 变量名 类型 = 值
	var a int = 10

	// 2) 省略类型,由右侧值推断
	var b = 20 // b 被推断为 int

	// 3) 只声明不赋值,自动赋"零值"
	var c int // c == 0

	// 4) 一次声明多个(同类型可共享类型名)
	var x, y int = 1, 2
	var name, age = "Tom", 18 // 类型可以不同,各自推断

	fmt.Println(a, b, c, x, y, name, age)
}

1.2 批量声明(var 块)

包级变量常用 var (...) 块统一组织,可读性更好:

var (
	appName    = "myapp"
	version    = "1.0.0"
	maxRetries = 3
	debug      bool // 零值 false
)

1.3 零值机制

Go 没有"未初始化"这个概念。任何变量声明后若不显式赋值,都会被赋予该类型的零值(zero value)。这是 Go “零值可用"设计哲学的基础——很多类型不初始化就能直接用。

类型分类 具体类型 零值
布尔 bool false
整型 int/int8~64/uint*/byte/rune 0
浮点 float32/float64 0.0
复数 complex64/complex128 0+0i
字符串 string ""(空串,非 nil)
指针 *T nil
切片 []T nil
map map[K]V nil
channel chan T nil
函数 func(...) nil
接口 interface{} / any nil
数组 [n]T 每个元素都是各自的零值
结构体 struct{...} 每个字段都是各自的零值
func main() {
	var (
		i int
		f float64
		s string
		p *int
		sl []int
		m map[string]int
	)
	fmt.Printf("%d %f %q %v %v %v\n", i, f, s, p, sl, m)
	// 输出:0 0.000000 "" <nil> [] map[]

	// 零值可用示例:nil 切片可以直接 append
	sl = append(sl, 1, 2, 3)
	fmt.Println(sl) // [1 2 3]

	// 但 nil map 不能直接写入,会 panic!
	// m["a"] = 1 // panic: assignment to entry in nil map
}

注意区别:nil 切片可以 append,nil map 不能直接写。这是零值可用的一个重要边界。


2. 短变量声明与变量遮蔽

2.1 := 的规则

:= 是短变量声明,只能用在函数内部,编译器自动推断类型:

func demo() {
	x := 42          // 等价 var x = 42
	name := "gopher" // string
	ok := true       // bool

	// 多变量短声明
	a, b := 1, 2

	// 特殊规则:左侧只要有一个是新变量即可复用 :=
	a, c := 10, 20 // a 是已存在变量(赋值),c 是新变量(声明)
	_ = c
	fmt.Println(x, name, ok, a, b)
}

2.2 var 与 := 的区别与选型

维度 var :=
使用位置 包级 + 函数内 仅函数内
是否需赋值 否(有零值) 是(必须有初值)
显式指定类型 可以 不可以(只能推断)
典型场景 包级变量、需要零值、需要指定精确类型 函数内局部变量、for/if 临时变量
// 需要精确类型时用 var(推断会给默认 int/float64)
var count int64 = 0
var ratio float32 = 0.5

// 局部临时变量用 := 更简洁
for i := 0; i < 10; i++ {
	v := i * i
	_ = v
}

2.3 变量遮蔽(Shadowing)陷阱

短声明最常见的坑:在内层作用域用 := 声明了与外层同名的变量,形成"遮蔽”,导致对外层变量的修改失效。

func shadowBug() (err error) {
	if true {
		// 本意是给外层 err 赋值,却因为 := 新建了一个内层 err!
		err := doSomething() // ← 遮蔽了返回值 err
		if err != nil {
			fmt.Println("内层捕获:", err)
		}
	}
	// 外层 err 始终是 nil,调用方永远拿不到错误
	return err
}

func doSomething() error { return fmt.Errorf("boom") }

正确写法:用 = 而非 :=,或提前声明。

func shadowFix() (err error) {
	if true {
		err = doSomething() // = 赋值给外层 err
		if err != nil {
			return err
		}
	}
	return nil
}

用 go vet 检测遮蔽

标准工具链自带 go vet,但 shadow 检查在独立分析器中,需要单独安装:

# 安装 shadow 分析器
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest

# 以 vettool 方式运行
go vet -vettool=$(which shadow) ./...

输出示例:

./main.go:5:3: declaration of "err" shadows declaration at line 3

实践建议:在 CI 中加上 shadow 检查;返回值命名 err 时,函数体内一律用 = 赋值给它。


3. 常量 const

3.1 常量的本质:编译期确定

常量的值必须在编译期就能计算出来,因此只能由字面量、其他常量、编译期可求值的表达式(含内置函数 lencap 对常量数组等)组成。

const Pi = 3.14159
const Greeting = "hello"

// 常量表达式:编译期计算
const (
	KB = 1 << 10 // 1024
	MB = 1 << 20 // 1048576
	Sec = 60
	Min = 60 * Sec
)

func main() {
	// 常量不能取地址(它没有运行期内存位置)
	// p := &Pi // 编译错误:cannot take the address of Pi

	// 常量不能被运行期的值赋值
	x := 10
	// const y = x // 编译错误:x (variable) is not constant
	_ = x
}

3.2 无类型常量(Untyped Constant)原理

这是 Go 常量系统最精妙的地方。一个不指定类型的常量是无类型常量,它以"任意精度"(arbitrary precision)保存,直到在具体上下文中被使用时才转换成某个具体类型。

const big = 1 << 62 // 无类型整型常量,精度极高

func main() {
	// 无类型常量能同时"适配"多种类型
	const c = 100
	var i int = c     // 当作 int
	var f float64 = c // 当作 float64
	var b byte = c    // 当作 byte(100 在 0~255 范围内)
	fmt.Println(i, f, b)

	// 无类型常量的运算保持高精度
	const third = 1.0 / 3.0 // 高精度浮点常量
	var x float64 = third   // 用到时才截断为 float64
	fmt.Println(x)
}

对比有类型常量:一旦指定类型,就丧失了"随上下文变形"的能力。

const typed int = 100
// var f float64 = typed // 编译错误:类型不匹配,需要显式转换 float64(typed)

const untyped = 100
var f float64 = untyped // OK:无类型常量自动适配

无类型常量分六类:无类型布尔、无类型 rune、无类型整数、无类型浮点、无类型复数、无类型字符串。它们各有一个默认类型(见本章第 8 节),只有在必须落地为具体类型(如赋值给 interface{}、用 := 声明)时才启用默认类型。


4. iota 枚举

Go 没有专门的 enum 关键字,用 const + iota 实现枚举。iota 是常量块内的行计数器:每个 const (...) 块开始时 iota 归 0,块内每新增一行(ConstSpec)自增 1。

4.1 基础自增

const (
	A = iota // 0
	B        // 1(沿用上一行表达式 iota,此时 iota=1)
	C        // 2
	D        // 3
)

关键点:常量块内若某行省略了 = 表达式,它会自动重复上一行的表达式,而 iota 每行都在递增。

4.2 跳值与占位

_ 跳过不需要的值,iota 照常递增:

const (
	_  = iota // 0,丢弃
	KB = 1 << (10 * iota) // iota=1 → 1<<10
	MB                    // iota=2 → 1<<20
	GB                    // iota=3 → 1<<30
	TB                    // iota=4 → 1<<40
)

func main() {
	fmt.Println(KB, MB, GB, TB)
	// 1024 1048576 1073741824 1099511627776
}

4.3 位运算枚举(标志位)

用左移生成互不重叠的位,可用按位或组合、按位与检测:

type Permission uint8

const (
	Read    Permission = 1 << iota // 1  (0b001)
	Write                          // 2  (0b010)
	Execute                        // 4  (0b100)
)

func main() {
	// 组合权限
	perm := Read | Write // 0b011 = 3

	// 检测是否含某权限
	fmt.Println(perm&Read != 0)    // true
	fmt.Println(perm&Execute != 0) // false

	// 用 &^ 清除某权限(见本章第 7 节 运算符)
	perm = perm &^ Write // 移除写权限
	fmt.Println(perm)    // 1
}

4.4 表达式技巧与多组常量

// 每行都能用表达式,iota 只是变量
const (
	_   = iota
	a   = iota * 10 // 10
	b               // 20(重复 iota*10,iota=2)
	c               // 30
)

// 同一行多个常量共享同一个 iota 值
const (
	x0, y0 = iota, iota + 10 // iota=0 → 0, 10
	x1, y1                   // iota=1 → 1, 11
	x2, y2                   // iota=2 → 2, 12
)

4.5 枚举加 String() 方法(工程实践)

裸的整型枚举打印出来是数字,不可读。惯例是配合 stringer 工具生成 String()

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
)

// go:generate stringer -type=Weekday
// 生成后:fmt.Println(Monday) 输出 "Monday" 而非 1
go install golang.org/x/tools/cmd/stringer@latest
go generate ./...

5. 作用域与生命周期

5.1 三级作用域

作用域层级(由外到内):

┌─────────────────────────────────────────┐  ← 包级作用域 package scope
│ Package scope                           │
│ var / const / func / type declared here │
│  ┌───────────────────────────────────┐  │  ← 函数级作用域 function scope
│  │ Function scope                    │  │
│  │ func params, vars declared in func│  │
│  │  ┌─────────────────────────────┐  │  │  ← 块级作用域 block scope
│  │  │ Block scope                 │  │  │
│  │  │ { } / if / for / switch     │  │  │
│  │  └─────────────────────────────┘  │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

变量查找:由内向外逐层查找,找到第一个即停(内层遮蔽外层)
var g = "package" // 包级

func scope() {
	f := "function" // 函数级
	{
		b := "block" // 块级,出了 {} 即不可见
		fmt.Println(g, f, b)
	}
	// fmt.Println(b) // 编译错误:b 未定义
}

5.2 if / for 的隐式块作用域

ifforswitch 的初始化语句声明的变量,作用域仅限该语句及其分支:

func main() {
	if v := compute(); v > 0 { // v 只在 if-else 内可见
		fmt.Println("正数", v)
	} else {
		fmt.Println("非正", v)
	}
	// fmt.Println(v) // 编译错误:v 已出作用域
}

func compute() int { return 1 }

5.3 生命周期与逃逸

作用域是"编译期可见性",生命周期是"运行期内存存活时长",二者不同。Go 有垃圾回收,变量只要还被引用就存活。局部变量若被外部引用(如返回其地址),编译器会做逃逸分析,把它分配到堆上:

func newCounter() *int {
	n := 0    // 局部变量
	return &n // 取地址返回 → n 逃逸到堆,函数返回后依然存活
}
# 查看逃逸分析结果
go build -gcflags="-m" main.go
# 输出:./main.go:2:2: moved to heap: n

5.4 可见性由首字母大小写决定

Go 用标识符首字母是否大写来控制包外可见性(导出/exported):

首字母 可见性 示例
大写 导出,包外可访问 PrintlnMaxIntUser
小写 未导出,仅包内可见 countparseuserID
package model

type User struct {
	Name string // 导出字段,其他包可读写
	age  int    // 未导出字段,仅 model 包内可访问
}

func NewUser(name string) *User { return &User{Name: name} } // 导出构造函数
func validate(u *User) bool      { return u.age >= 0 }        // 包内辅助函数

6. 命名规范

Go 官方对命名有一套约定,golint/revive 会检查。核心原则:简洁 + 语境

6.1 通用规则

  • 使用驼峰命名(camelCase / PascalCase),不用下划线:userName 而非 user_name
  • 导出用大写驼峰 UserName,未导出用小写驼峰 userName
  • 变量名长度与作用域成正比:循环里用 iv,包级变量用完整名字。

6.2 缩写词全大写或全小写

缩写词(ID、URL、HTTP、API)保持大小写一致,不要写成 UrlHttp

// 推荐
var userID int
var apiURL string
type HTTPServer struct{}
func ServeHTTP() {}

// 不推荐
var userId int      // 应为 userID
var apiUrl string   // 应为 apiURL
type HttpServer struct{} // 应为 HTTPServer

6.3 包名

  • 全小写、单个单词、无下划线无复数:httpstrconvuser
  • 包名应与目录名一致。
  • 避免与标准库/常见变量冲突的名字(如别用 utilcommon 这种无意义大杂烩)。
// 调用时包名是前缀,所以包内不用再重复包名
package user
func New() *User {} // 调用方写 user.New(),而不是 user.NewUser()

6.4 receiver 命名约定

方法接收者用 1~2 个字母的短名,通常取类型名首字母缩写,且同一类型的所有方法保持一致

type Server struct{ addr string }

// 推荐:短且一致,全用 s
func (s *Server) Start() {}
func (s *Server) Stop()  {}

// 不推荐:忽长忽短、用 this/self(这不是 Go 风格)
// func (this *Server) Start() {}
// func (server *Server) Stop() {}

6.5 golint 常见规则速查

规则 反例 正例
缩写词大小写 Id ID
不用下划线 my_func myFunc
receiver 一致 this/self 类型首字母
导出需注释 无注释的 Foo // Foo does ...
错误变量名 Err/e err
错误类型 MyError xxxError
错误值 MyErr ErrXxx

7. 运算符

7.1 算术运算符

func main() {
	fmt.Println(7 + 3) // 10
	fmt.Println(7 - 3) // 4
	fmt.Println(7 * 3) // 21
	fmt.Println(7 / 3) // 2   ← 整数除法向零截断
	fmt.Println(7 % 3) // 1   ← 取模

	// 浮点除法
	fmt.Println(7.0 / 3.0) // 2.3333333333333335

	// Go 没有 ++x / x++ 作为表达式,只能作为语句
	x := 1
	x++ // 正确:语句
	// y := x++ // 编译错误:x++ 不是表达式
	_ = x
}

注意:Go 中 ++/-- 只能是独立语句,不能出现在表达式里,也没有前缀形式 ++x

7.2 比较与逻辑运算符

func main() {
	// 比较运算符返回 bool
	fmt.Println(1 == 1, 1 != 2, 1 < 2, 2 >= 2)

	// 逻辑运算:&& 和 || 有短路特性
	a, b := true, false
	fmt.Println(a && b) // false
	fmt.Println(a || b) // true
	fmt.Println(!a)     // false

	// 短路:左侧已能确定结果,右侧不再求值
	if b && expensiveCheck() { // b 为 false,expensiveCheck 不执行
	}
}

func expensiveCheck() bool { fmt.Println("被调用"); return true }

7.3 位运算符(重点:&^)

func main() {
	const (
		a = 0b1100 // 12
		b = 0b1010 // 10
	)
	fmt.Printf("%04b\n", a&b)  // 1000  按位与
	fmt.Printf("%04b\n", a|b)  // 1110  按位或
	fmt.Printf("%04b\n", a^b)  // 0110  按位异或
	fmt.Printf("%04b\n", a<<1) // 11000 左移
	fmt.Printf("%04b\n", a>>1) // 0110  右移

	// &^ 按位清零(AND NOT / bit clear)
	// a &^ b:b 中为 1 的位,在结果中被清 0;b 中为 0 的位,保留 a 原值
	fmt.Printf("%04b\n", a&^b) // 0100
}

&^(bit clear)等价于 a & (^b),常用于从标志位集合中移除某些标志(见 4.3 权限示例),比 perm & ^Write 少一次取反书写,语义更清晰。

a &^ b 真值表(逐位):
  a=1 b=1 → 0   (b 为 1 则清零)
  a=1 b=0 → 1   (b 为 0 则保留 a)
  a=0 b=1 → 0
  a=0 b=0 → 0

7.4 没有三元运算符

Go 刻意不提供 a ? b : c,官方理由是三元表达式容易被滥用成难读的嵌套。替代方案是 if-else

// 其他语言:max := a > b ? a : b
// Go 写法:
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// Go 1.21+ 起,内置泛型函数 max/min 可直接用
func main() {
	fmt.Println(max(3, 5)) // 5,也可直接用内置 max(3,5)
}

7.5 运算符优先级

Go 只有 5 个优先级,比 C 简单得多(数字越大越先算):

优先级 运算符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
// & 与 == 优先级:& 高于 ==,所以下面按预期工作
if flags&mask == mask { // (flags & mask) == mask
}

8. 类型推断与默认类型

8.1 类型推断原理

Go 编译器在 :=var x = 值 时,根据右侧表达式的类型来推断变量类型。若右侧是有类型的值,直接沿用;若是无类型常量,则采用该常量的默认类型。

func main() {
	var f float64 = 3.14
	x := f // x 是 float64(沿用右侧有类型值)

	y := 3.14 // 3.14 是无类型浮点常量 → 默认类型 float64
	z := 42    // 42 是无类型整型常量 → 默认类型 int

	fmt.Printf("%T %T %T\n", x, y, z) // float64 float64 int
}

8.2 无类型常量的默认类型表

当无类型常量必须"落地"为具体类型(如通过 := 声明、传给 interface{} 参数)时,采用如下默认类型:

无类型常量种类 默认类型 示例
布尔 bool true
rune(字符) rune(int32) 'A'
整数 int 42
浮点 float64 3.14
复数 complex128 1+2i
字符串 string "hi"
func main() {
	i := 42     // int
	r := 'A'    // rune (int32)
	f := 3.14   // float64
	c := 1 + 2i // complex128
	s := "hi"   // string
	b := true   // bool
	fmt.Printf("%T %T %T %T %T %T\n", i, r, f, c, s, b)
	// int int32 float64 complex128 string bool
}

8.3 类型转换必须显式

Go 不做隐式数值转换(与 C 不同),不同类型运算必须显式转换,否则编译报错:

func main() {
	var i int = 10
	var f float64 = 3.5
	// sum := i + f // 编译错误:mismatched types int and float64
	sum := float64(i) + f // 显式转换
	fmt.Println(sum)      // 13.5

	// int → int64 也要显式
	var big int64 = int64(i)
	_ = big

	// 但无类型常量不受此限(编译期适配)
	var g float64 = 10 // OK:10 是无类型常量
	_ = g
}

9. 高频面试题

9.1 := 与 var 有什么区别?

  1. var 可用于包级和函数内,:= 只能用于函数内。
  2. var 可只声明不赋值(取零值),:= 必须有初值。
  3. var 可显式指定类型,:= 只能靠推断(得到默认类型)。
  4. := 左侧要求至少有一个新变量;若全是旧变量应用 =

9.2 下面这段 iota 输出什么?

const (
	a, b = iota, iota + 1 // iota=0
	c, d                  // iota=1
	e, f                  // iota=2
)

a=0 b=1c=1 d=2e=2 f=3。同一行两个常量共享当前 iota 值;下一行省略表达式则重复上一行的表达式 iota, iota+1,而 iota 每行加 1。

9.3 什么是无类型常量?有什么用?

:不指定类型的常量以任意精度保存,直到使用时才转换成具体类型。好处:

  1. 一个常量能适配多种数值类型,无需为每种类型定义一份。
  2. 常量运算保持高精度,避免中间截断。
  3. 数学常量(如 math.Pi)能同时用于 float32/float64 场景。 一旦给常量指定了类型,就失去这种"变形"能力。

9.4 下面代码有什么 bug?

func read() (data string, err error) {
	if f, err := open(); err == nil {
		data = process(f)
	}
	return
}

:变量遮蔽。if f, err := open() 中的 err 是新声明的内层变量,遮蔽了命名返回值 err。当 open() 返回错误时,内层 err 被赋值但外层返回值 err 始终是 nil,调用方拿不到错误。修复:把 f, err := 改为先声明 f、再对返回值 err=,或整体重构。可用 go vet -vettool=$(which shadow) 检测。

9.5 nil 切片和 nil map 有什么区别?

  • nil 切片可以直接 append(append 会分配底层数组),len/range 也安全。
  • nil map 可以读(返回零值)和 range(空循环),但写入会 panic,必须先 make。 这体现了 Go “零值可用"的边界:读侧尽量友好,写侧需要显式初始化 map。

9.6 Go 为什么没有三元运算符?没有隐式类型转换?

  • 无三元运算符:官方认为 ?: 易被嵌套滥用降低可读性,坚持用 if-else,语言更少即更清晰(Go 1.21 后可用内置 max/min 覆盖常见场景)。
  • 无隐式转换:避免 C 语言中隐式转换引发的精度丢失、符号问题等隐蔽 bug,强制显式转换让类型意图清晰。但无类型常量在编译期可自动适配,兼顾了便利与安全。

9.7 &^ 运算符是什么?

&^ 是"按位清零”(AND NOT / bit clear)。a &^ b 会把 b 中为 1 的位在结果里清 0,其余位保留 a 的值,等价于 a & (^b)。常用于从标志位集合中移除特定标志,比手写取反更直观。

9.8 什么是"零值可用"设计?举例。

:让类型不经初始化就能安全使用。典型例子:

  • sync.Mutex 零值即可直接 Lock,无需构造。
  • bytes.Buffer 零值可直接 Write
  • sync.WaitGroupsync.Once 均零值可用。 设计自己的类型时也应尽量让零值有意义,减少调用方的初始化负担和出错概率。

小结

  • 变量声明var 通用(包级/函数级、可零值、可指定类型),:= 只能在函数内且必须有初值;任何变量不赋值都有确定的零值,这是"零值可用"的基础。
  • 变量遮蔽是短声明最大的坑,尤其对命名返回值 err,用 go vet -vettool=$(which shadow) 检测。
  • 常量编译期确定、不可取地址;无类型常量以任意精度保存、随上下文变形,是 Go 常量系统的精髓。
  • iota 是常量块内的行计数器,配合左移做位标志、配合 _ 跳值、配合 stringer 生成可读枚举。
  • 作用域分包级/函数级/块级,由内向外查找;可见性由标识符首字母大小写决定。
  • 命名遵循驼峰、缩写词全大写(ID/URL)、包名全小写、receiver 短名一致。
  • 运算符没有三元表达式、没有隐式转换,++/-- 只能作语句;牢记 &^ 按位清零。
  • 类型推断优先沿用有类型值,否则采用无类型常量的默认类型(整数→int、浮点→float64)。

下一章将深入 Go 的基本数据类型:整型、浮点、byte/rune 与类型转换的种种细节。