2. Go语言核心语法:变量与常量、基本数据类型、复合数据类型、流程控制、函数定义与多返回值
好,我们直接进入正题。Go语言的语法设计理念就是「少即是多」。你写多了Java或Python,再回头看Go,会觉得它很「素」。但正是这种素,让代码的可读性和维护性变得极高。我个人非常喜欢这种风格——没有花里胡哨的语法糖,写起来很踏实。
2.1 变量与常量:声明的方式决定了你的代码风格
Go的变量声明有好几种方式。我刚开始用Go的时候,总觉得「var」关键字多余,后来才发现,不同场景用不同方式,代码会干净很多。
核心原则:能用短变量声明(:=)的地方,就别写var。但包级别变量,请老老实实用var。
// 包级别变量
var name string = "Go"
var version = 1.21 // 类型推断
// 函数内部:短变量声明
func main() {
count := 10
msg := "Hello"
// 批量声明
var x, y int = 1, 2
}
常量用const,这个没什么好说的。但有个小技巧:iota。我在项目中用它来定义枚举值,比手写数字优雅多了。
const (
StatusPending = iota // 0
StatusRunning // 1
StatusDone // 2
)
我的习惯:常量名用驼峰,不要全大写。Go社区不流行Java那种STATUS_PENDING风格。
2.2 基本数据类型:int、float、string、bool
Go的基本类型很精简。int在64位系统上是64位,32位系统上是32位。嗯,这里要注意:如果你需要明确位数,请用int32或int64。
| 类型 | 说明 | 零值 |
|---|---|---|
| int / int32 / int64 | 整数,默认int | 0 |
| float32 / float64 | 浮点数,默认float64 | 0.0 |
| string | 字符串,不可变 | "" |
| bool | 布尔值 | false |
字符串有个坑:它是只读的。你不能像C语言那样直接修改某个字符。我曾经在项目中想直接改字符串里的某个字节,结果编译报错。正确做法是转成[]byte再改。
s := "hello"
// s[0] = 'H' // 编译错误!
b := []byte(s)
b[0] = 'H'
s = string(b) // "Hello"
2.3 复合数据类型:array、slice、map、struct
这部分是Go的核心,也是你写微服务时天天打交道的东西。
2.3.1 数组 vs 切片
数组是固定长度的,切片是动态的。说实话,我在项目中几乎不用数组,除非是定义固定大小的缓冲区。切片才是王道。
// 数组:长度固定
arr := [3]int{1, 2, 3}
// 切片:动态长度
slice := []int{1, 2, 3}
slice = append(slice, 4) // 没问题
我曾经踩过的坑:切片是引用类型,赋值给另一个变量时,它们共享底层数组。改一个,另一个也会变。深拷贝请用copy()函数。
2.3.2 map:键值对容器
map用之前必须初始化,否则就是nil。你想想看,往nil map里写数据,直接panic。我刚开始学Go时就被这个坑过。
// 正确初始化
m := make(map[string]int)
m["age"] = 30
// 或者直接字面量
m2 := map[string]int{"age": 30}
读取不存在的key不会报错,返回零值。所以判断key是否存在要用「逗号ok」模式:
if val, ok := m["name"]; ok {
// key存在
} else {
// key不存在
}
2.3.3 struct:自定义类型
struct是Go里唯一支持自定义字段的类型。没有class,没有继承。但别担心,组合比继承更灵活。
type User struct {
Name string
Age int
}
u := User{
Name: "张三",
Age: 25,
}
我的建议:struct字段名大写表示公开,小写表示私有。这在JSON序列化时特别重要,配合json:"field_name"标签使用。
2.4 流程控制:if、for、switch
Go的流程控制很简洁。没有while,没有do-while,只有for。但for可以玩出花来。
2.4.1 if:可以带初始化语句
if err := doSomething(); err != nil {
// 处理错误
return
}
// err在这里不可见
这种写法在Go里非常常见。err的作用域被限制在if块内,避免了变量污染。我个人很喜欢这种设计。
2.4.2 for:唯一的循环
// 经典三段式
for i := 0; i < 10; i++ {
}
// 类似while
for i < 10 {
}
// 无限循环
for {
break
}
// range遍历
for index, value := range slice {
}
range遍历时,value是拷贝。如果你想修改原值,请用index访问。
2.4.3 switch:不需要break
Go的switch默认只执行一个case,不会自动穿透。这省了多少bug啊!
switch status {
case 1:
fmt.Println("pending")
case 2:
fmt.Println("running")
default:
fmt.Println("unknown")
}
如果你想穿透,用fallthrough关键字。但说实话,我很少用。
2.5 函数定义与多返回值
函数是Go的一等公民。多返回值是Go最实用的特性之一,没有它,错误处理会变得很别扭。
// 多返回值:通常最后一个返回error
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为0")
}
return a / b, nil
}
// 命名返回值
func sum(a, b int) (result int) {
result = a + b
return // 裸返回
}
重要:命名返回值虽然方便,但会降低代码可读性。我建议只在简单函数中使用。复杂函数还是显式return比较好。
函数也可以作为参数传递,这就是回调。Go没有泛型(1.18之前),但通过函数参数可以实现类似的效果。
func apply(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, v := range nums {
result[i] = fn(v)
}
return result
}
// 使用
doubled := apply([]int{1, 2, 3}, func(x int) int {
return x * 2
})
好了,这一章的内容就这些。变量声明、基本类型、复合类型、流程控制、函数——这些是Go的骨架。下一章我们会聊函数的高级用法,比如闭包、defer、panic/recover。这些东西在实际项目中非常有用,尤其是defer,我几乎每个函数都会用到。