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,我几乎每个函数都会用到。