← 返回目录

第二章:基础语法

变量与类型、控制流与错误处理

1. 变量与类型

Go 是静态类型语言,变量类型在编译时确定。支持显式声明和类型推断两种方式。

变量声明

package main

import "fmt"

func main() {
    // var 声明(显式类型)
    var name string = "Go"
    var age int = 15

    // var 声明(类型推断)
    var version = 1.24
    var active = true

    // 短声明 :=(最常用,仅限函数内部)
    lang := "Go"
    count := 42
    pi := 3.14159

    // 多变量声明
    var x, y int = 10, 20
    a, b := "hello", "world"

    // 声明块
    var (
        host   string = "localhost"
        port   int    = 8080
        debug  bool   = true
    )

    fmt.Println(name, age, version, active)
    fmt.Println(lang, count, pi)
    fmt.Println(x, y, a, b)
    fmt.Println(host, port, debug)
}

基本类型

// 整数类型
var i int = 42         // 平台相关:32位或64位
var i8 int8 = 127      // -128 ~ 127
var i64 int64 = 9999   // 64位整数
var u uint = 42        // 无符号整数

// 浮点数
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// 布尔
var ok bool = true

// 字符串
var s string = "Hello, 世界"

// byte 和 rune
var b byte = 'A'       // uint8 的别名
var r rune = '中'      // int32 的别名,表示 Unicode 码点

零值与类型转换

// Go 的零值:声明但未赋值的变量自动获得零值
var i int       // 0
var f float64   // 0.0
var s string    // ""(空字符串)
var b bool      // false
var p *int      // nil

// 类型转换必须显式进行
var x int = 42
var y float64 = float64(x)     // int -> float64
var z int = int(y)             // float64 -> int(截断小数)
var s string = fmt.Sprintf("%d", x) // int -> string

常量

const Pi = 3.14159
const AppName = "MyApp"

// 常量块
const (
    StatusOK    = 200
    StatusError = 500
)

// iota: 自增常量生成器
const (
    Sunday    = iota // 0
    Monday           // 1
    Tuesday          // 2
    Wednesday        // 3
    Thursday         // 4
    Friday           // 5
    Saturday         // 6
)

重要:Go 不允许隐式类型转换。intint64 是不同类型,必须显式转换。未使用的变量会导致编译错误。

2. 字符串

Go 的字符串是不可变的 UTF-8 编码字节序列。

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 基本操作
    s := "Hello, 世界"
    fmt.Println(len(s))           // 13(字节数,不是字符数)
    fmt.Println(len([]rune(s)))   // 9(字符数)

    // 字符串拼接
    greeting := "Hello" + ", " + "Go"
    formatted := fmt.Sprintf("版本: %d.%d", 1, 24)

    // 多行字符串(反引号,原始字符串)
    sql := `SELECT *
FROM users
WHERE age > 18
ORDER BY name`

    // strings 包常用函数
    fmt.Println(strings.Contains("golang", "go"))       // true
    fmt.Println(strings.HasPrefix("golang", "go"))      // true
    fmt.Println(strings.HasSuffix("main.go", ".go"))    // true
    fmt.Println(strings.ToUpper("hello"))                // HELLO
    fmt.Println(strings.Split("a,b,c", ","))             // [a b c]
    fmt.Println(strings.Join([]string{"a", "b"}, "-"))   // a-b
    fmt.Println(strings.Replace("foo bar", "foo", "baz", 1)) // baz bar
    fmt.Println(strings.TrimSpace("  hello  "))          // hello

    fmt.Println(greeting, formatted, sql)
}

// rune vs byte
func runeDemo() {
    s := "Go语言"
    // 按字节遍历
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    // 按字符(rune)遍历
    for i, r := range s {
        fmt.Printf("索引%d: %c\n", i, r)
    }
}

rune vs byte:byteuint8 的别名,处理 ASCII。runeint32 的别名,处理 Unicode 字符。用 range 遍历字符串时自动按 rune 迭代。

3. 数组与切片

Go 有固定长度的数组和动态长度的切片。实际开发中几乎总是使用切片。

package main

import "fmt"

func main() {
    // 数组:固定长度,值类型
    var arr [3]int = [3]int{1, 2, 3}
    arr2 := [...]int{10, 20, 30, 40} // 编译器推断长度

    // 切片:动态长度,引用类型
    nums := []int{1, 2, 3, 4, 5}

    // make 创建切片(长度, 容量)
    s := make([]int, 5)       // len=5, cap=5
    s2 := make([]int, 0, 10)  // len=0, cap=10

    // 切片操作
    sub := nums[1:3]   // [2, 3](左闭右开)
    head := nums[:3]   // [1, 2, 3]
    tail := nums[2:]   // [3, 4, 5]

    // append 追加元素
    s2 = append(s2, 1, 2, 3)
    s2 = append(s2, nums...) // 展开追加

    // 遍历
    for i, v := range nums {
        fmt.Printf("索引 %d: 值 %d\n", i, v)
    }
    // 只需要值
    for _, v := range nums {
        fmt.Println(v)
    }
    // 只需要索引
    for i := range nums {
        fmt.Println(i)
    }

    // copy
    dst := make([]int, len(nums))
    copy(dst, nums)

    fmt.Println(arr, arr2, s, sub, head, tail, dst)
}

🔄 动态数组对比

语言 类型 追加
Go []int (切片) append(s, 1)
Python list lst.append(1)
Java ArrayList list.add(1)
JavaScript Array arr.push(1)

Go 的 append 返回新切片,必须赋值回去:s = append(s, 1)

4. Map

Map 是 Go 内置的哈希表类型,存储键值对。

package main

import "fmt"

func main() {
    // 声明与初始化
    m := map[string]int{
        "apple":  5,
        "banana": 3,
        "cherry": 8,
    }

    // make 创建空 map
    scores := make(map[string]int)

    // 增 / 改
    scores["Alice"] = 95
    scores["Bob"] = 87
    scores["Alice"] = 98 // 更新

    // 查(comma ok 模式)
    val, ok := scores["Alice"]
    if ok {
        fmt.Printf("Alice: %d\n", val)
    }

    // 检查 key 是否存在
    if _, exists := scores["Charlie"]; !exists {
        fmt.Println("Charlie 不存在")
    }

    // 删
    delete(scores, "Bob")

    // 遍历(顺序不固定)
    for key, value := range m {
        fmt.Printf("%s: %d\n", key, value)
    }

    // 获取长度
    fmt.Println("元素数量:", len(m))
}

comma ok 模式:访问 map 返回两个值 val, okoktrue 表示 key 存在。这是 Go 中避免零值歧义的惯用方式。

5. 控制流

if / else

// 基本 if
if x > 0 {
    fmt.Println("正数")
} else if x < 0 {
    fmt.Println("负数")
} else {
    fmt.Println("零")
}

// if 带初始化语句(变量作用域限定在 if 块内)
if err := doSomething(); err != nil {
    fmt.Println("错误:", err)
}

// 文件操作中常见
if f, err := os.Open("file.txt"); err != nil {
    log.Fatal(err)
} else {
    defer f.Close()
    // 使用 f...
}

for(Go 唯一的循环)

// 传统 for
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// while 风格
n := 1
for n < 100 {
    n *= 2
}

// 无限循环
for {
    // break 或 return 退出
    break
}

// range 遍历
nums := []int{10, 20, 30}
for i, v := range nums {
    fmt.Printf("%d: %d\n", i, v)
}

// 遍历字符串(按 rune)
for i, ch := range "Go语言" {
    fmt.Printf("%d: %c\n", i, ch)
}

// 遍历 map
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
    fmt.Printf("%s=%d\n", k, v)
}

switch(无需 break)

// 基本 switch(自动 break,不会穿透)
day := "Monday"
switch day {
case "Monday":
    fmt.Println("星期一")
case "Saturday", "Sunday":
    fmt.Println("周末")
default:
    fmt.Println("工作日")
}

// 无条件 switch(替代长 if-else 链)
score := 85
switch {
case score >= 90:
    fmt.Println("优秀")
case score >= 80:
    fmt.Println("良好")
case score >= 60:
    fmt.Println("及格")
default:
    fmt.Println("不及格")
}

// fallthrough 显式穿透到下一个 case
switch 3 {
case 3:
    fmt.Println("三")
    fallthrough
case 4:
    fmt.Println("四(也会执行)")
}

defer

// defer 延迟执行,函数返回时按 LIFO 顺序执行
func readFile() {
    f, err := os.Open("data.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // 确保文件关闭

    // 读取文件...
}

// 多个 defer 按 LIFO 执行
func demo() {
    defer fmt.Println("1") // 最后执行
    defer fmt.Println("2") // 倒数第二
    defer fmt.Println("3") // 最先执行
    // 输出: 3, 2, 1
}

Go 的特殊之处:for 是唯一的循环关键字,没有 whiledo-whileswitch 默认不穿透,需要 fallthrough 显式声明。条件表达式不需要括号。

6. 错误处理

Go 没有异常机制(try/catch),使用多返回值 + error 接口进行错误处理。这是 Go 最具争议但也最核心的设计。

package main

import (
    "errors"
    "fmt"
    "strconv"
)

// error 接口只有一个方法
// type error interface {
//     Error() string
// }

// 函数返回 error
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为零")
    }
    return a / b, nil
}

// fmt.Errorf 带格式化的错误
func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("解析年龄失败 %q: %w", s, err)
    }
    if age < 0 || age > 150 {
        return 0, fmt.Errorf("年龄超出范围: %d", age)
    }
    return age, nil
}

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}

func validateName(name string) error {
    if len(name) == 0 {
        return &ValidationError{Field: "name", Message: "不能为空"}
    }
    return nil
}

func main() {
    // 基本错误检查模式
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }

    // errors.Is 和 errors.As(Go 1.13+)
    age, err := parseAge("abc")
    if err != nil {
        fmt.Println(err) // 解析年龄失败 "abc": strconv.Atoi: ...
    }

    // 类型断言检查具体错误类型
    err = validateName("")
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("字段: %s, 消息: %s\n", ve.Field, ve.Message)
    }

    fmt.Println(age, result)
}

🔄 错误处理对比

语言 方式
Go 多返回值 val, err := fn(),手动检查 if err != nil
Java/Python/JS try { ... } catch(e) { ... } 异常机制
Rust Result<T, E> + ? 操作符

Go 的 if err != nil 模式虽然冗长,但让错误处理路径清晰可见,不会因忘记 catch 而遗漏错误。

7. 本章要点

📌 变量与类型

:= 短声明最常用,静态类型不允许隐式转换,零值机制避免了未初始化问题。

📌 字符串

不可变的 UTF-8 字节序列。rune 处理 Unicode,strings 包提供丰富的操作函数。

📌 切片与 Map

切片是动态数组,append 追加元素。Map 是哈希表,comma ok 模式检查 key 存在。

📌 控制流

for 是唯一循环,switch 默认不穿透,defer 延迟执行用于资源清理。

📌 错误处理

无异常机制,使用 error 接口 + 多返回值。if err != nil 是 Go 的标志性模式。

📌 iota 与常量

const 定义常量,iota 生成自增枚举值,常用于定义状态码和标志位。