Skip to content
Go
Go 基础语法

Go 基础语法

变量、常量、基本数据类型、零值、类型转换、iota、内建函数 min/max/clear、字符串操作等 Go 语言基础速查(基线 Go 1.27)。


1. 关键字与标识符

25 个关键字

类别 关键字
声明 var const type func package import
复合类型 struct interface map chan
控制流 if else for switch case default select
跳转 break continue fallthrough goto return
并发 go defer
其他 range

预定义标识符(非关键字,可被覆盖但不推荐)

类别 标识符
基本类型 bool byte string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128
常量 true false iota nil
内置函数 make len cap new append copy close delete panic recover print println real imag complex min max clear

注意:min max clear 是 Go 1.21 加入的内置函数,模块 go 指令低于 1.21 时编译报 built-in min requires go1.21 or later(实测),详见第 3 节。


2. 变量

var 声明

var name string = "Go"        // 完整形式:var 名称 类型 = 值
var age int                    // 声明但未初始化,等于零值(0)
var name, age = "Go", 14      // 多变量声明 + 类型推断
var (                         // 批量声明块
    name   string = "Go"
    version int    = 23
    ok     bool   = true
)

短变量声明 :=(函数内专用)

// 只能在函数内使用!
name := "Go"                  // 类型推断为 string
age := 14                     // 类型推断为 int
name, ok := getValue()        // 部分变量已存在时,仅声明新变量

注意::= 不能用于包级作用域。包级变量必须使用 var 关键字声明。

new 函数(Go 1.26 增强)

// Go 1.25 及之前:new(T) 仅接受类型,返回 *T(零值)
p := new(int)                     // *int,指向 0
user := new(User)                 // *User,所有字段零值

// Go 1.26+:new 支持带初始值的表达式,new(T) 旧用法不变
p := new(int64(300))              // *int64,指向 300(一行创建带值指针,实测)
u := new(User{Name: "Alice"})     // *User,复合字面量表达式同样合法(实测)

// 实用场景:JSON 可选字段
type Config struct {
    Timeout *int64 `json:"timeout,omitempty"`
}
cfg := Config{Timeout: new(int64(30))}  // Go 1.26:简洁
// 之前需要:t := int64(30); cfg := Config{Timeout: &t}

注意:new(expr) 受语言版本门控——go.mod 的 go 指令低于 1.26 时编译报 new(int64(300)) requires go1.26 or later(go1.27 实测)。建议场景:所有需要"分配并取地址带初值"的地方,尤其 omitempty 指针字段的单行初始化。

变量作用域 vs 短声明复用

func demo() {
    x := 10
    if true {
        x := 20               // 新的局部 x,外部 x 不变(:= 总是声明新变量)
        fmt.Println(x)        // 20
    }
    fmt.Println(x)            // 10

    // vs 赋值(复用同一变量)
    y := 10
    if true {
        y = 20                // 修改外部 y
    }
    fmt.Println(y)            // 20

    // := 部分复用
    f, err := os.Open("a.txt")   // f 和 err 都是新的
    f2, err := os.Open("b.txt")  // err 复用,f2 是新的(err 在同一作用域)
}

类型推断

a := 42                       // int(默认整数类型)
b := 3.14                     // float64(默认浮点类型)
c := "hello"                  // string
d := 1 + 2i                   // complex128
e := 'A'                      // rune(int32 别名)

3. 内置函数 min / max / clear(Go 1.21)

Go 1.21 把三个高频操作从"手写循环 / 删键循环"升级为语言级内建函数。类比 Python 的 min/max,Go 版支持任意多个同类型有序参数;clear 则统一了"清空 map"和"切片全部置零"两件事。

package main

import (
	"fmt"
	"math"
)

func main() {
	// min / max:两个及以上参数,类型必须是同一有序类型(数值、字符串)
	fmt.Println(min(3, 1, 2), max(3, 1, 2))               // 1 3
	fmt.Println(min("apple", "banana"))                   // apple(字符串按字典序)

	// 无类型常量参数会自动适配目标类型
	x := min(3, 1.5)
	fmt.Printf("%v (%T)\n", x, x)                         // 1.5 (float64)

	// clear:map 删除所有键;切片所有元素置零值
	m := map[string]int{"a": 1, "b": 2}
	clear(m)
	fmt.Println(len(m), m == nil)                         // 0 false(map 仍在,只是空了)

	s := []int{1, 2, 3, 4}
	clear(s)
	fmt.Println(s, len(s), cap(s))                        // [0 0 0 0] 4 4(长度容量不变)

	// NaN 特殊行为:只要参与比较,min/max 都返回 NaN
	nan := math.NaN()
	fmt.Println(min(nan, 1.0), max(nan, 1.0))             // NaN NaN
}

以上输出为 go1.27.0 实测结果。

边界与规则

  • 类型要求:参数只能是同一有序类型。无类型常量可以适配(min(1, int64(2)) 合法);两个不同类型的变量混用直接编译错误 mismatched types int (previous argument) and int64 (type of j)(实测)。
  • NaN 陷阱:浮点参数里只要有一个 NaN,min 和 max 都返回 NaN(实测),不能用来"兜底取安全值"。
  • clear 的参数范围:只接受 map 或 slice(含类型参数被约束为 map/slice 的情形)。数组不行——clear 一个 [3]int 编译报 argument must be (or constrained by) map or slice(go1.27 实测)。数组置零请用 var arr [3]int 重新赋值整体。
  • clear 对 nil:clear nil map 或 nil slice 是安全的空操作,不 panic(实测)。
  • clear 语义差别:map 是"删除所有键",底层桶可复用;slice 是"逐元素写零值",若元素是指针/结构体,这一步会真实释放引用,比 s = s[:0] 更利于 GC。
  • 版本门控:go.mod go 指令低于 1.21 时报 built-in min requires go1.21 or later / clear requires go1.21 or later(实测)。

生产里 min/max 最常见于钳制配置值、计算超时上限;clear 常见于对象池回收、复用 map 避免重复分配。


4. 常量与 iota

常量声明

const Pi = 3.14159                   // 无类型常量(更多灵活性)
const MaxSize int64 = 1 << 32        // 有类型常量

const (                              // 批量声明
    StatusOK    = 200
    StatusNotFound = 404
)

// iota:递增枚举器
const (
    Sunday = iota                    // 0
    Monday                           // 1
    Tuesday                          // 2
    Wednesday                        // 3
    Thursday                         // 4
    Friday                           // 5
    Saturday                         // 6
)

iota 进阶用法

// 位掩码
const (
    FlagRead    = 1 << iota         // 1 (1 << 0)
    FlagWrite                       // 2 (1 << 1)
    FlagExec                        // 4 (1 << 2)
)

// 跳值
const (
    _ = iota                        // 0 跳过
    KB = 1 << (10 * iota)           // 1 << 10 = 1024
    MB                              // 1 << 20 = 1048576
    GB                              // 1 << 30
)

// 多表达式每行
const (
    X, Y = iota, iota * 2           // 0, 0
    A, B                            // 1, 2
    C, D                            // 2, 4
)

最佳实践:使用 iota 替代硬编码的魔法数字。对于枚举类型的常量,用 iota + 有类型常量,可获取编译期类型安全。

无类型常量的精度

const huge = 1 << 100               // 无类型整数常量,精度远超 int64
fmt.Println(huge / (1 << 99))       // 2 — 编译期计算,不溢出

var x int64 = 1 << 62               // OK
var y int64 = 1 << 63               // 溢出 → 编译错误

数字字面量:进制前缀与下划线分隔

自 Go 1.13 起,整数字面量支持二进制 0b、八进制 0o 前缀,以及任意位置的 _ 分隔符(纯粹为人眼服务,编译器忽略):

b := 0b1010          // 二进制 → 10
o := 0o777           // 八进制 → 511
h := 0xFF            // 十六进制 → 255
n := 1_000_000       // 下划线分隔 → 1000000
m := 0xDE_AD_BE_EF   // 十六进制里同样可用 → 3735928559
f := 6.02e23         // 浮点科学计数(Println 输出 6.02e+23)

以上数值均为 go1.27 实测。老式八进制 0777(无前缀)仍然合法,但可读性差,新代码统一用 0o。分隔符不能贴着小数点或字母前缀(1._5、0_b1 非法)。

注意:下划线字面量受语言门控——go 指令为 1.12 及以下时报 underscore in numeric literal requires go1.13 or later(实测)。


5. 基本数据类型

类型速查

类型 大小 范围 零值
bool 1 byte true / false false
int8 1 byte -128 ~ 127 0
int16 2 bytes -32768 ~ 32767 0
int32 / rune 4 bytes -2^31 ~ 2^31-1 0
int64 8 bytes -2^63 ~ 2^63-1 0
uint8 / byte 1 byte 0 ~ 255 0
uint16 2 bytes 0 ~ 65535 0
uint32 4 bytes 0 ~ 2^32-1 0
uint64 8 bytes 0 ~ 2^64-1 0
int / uint 4 或 8 bytes 取决于平台 0
float32 4 bytes IEEE 754 0.0
float64 8 bytes IEEE 754 0.0
complex64 8 bytes 实部+虚部各 float32 0+0i
complex128 16 bytes 实部+虚部各 float64 0+0i
string 16 bytes (头) UTF-8 ""
uintptr 4 或 8 bytes 指针大小的无符号整数 0

选择指南:整数默认用 int;明确大小要求用 int64/int32;字节用 byte(uint8);Unicode 码点用 rune(int32)。浮点默认 float64。

类型别名 vs 类型定义

// 类型别名(Go 1.9+):完全等同原类型
type MyInt = int                  // MyInt 和 int 完全相同
var a MyInt = 10
var b int = a                     // OK

// 类型定义:创建新类型
type MyInt int                    // MyInt 是基于 int 的新类型
var a MyInt = 10
var b int = a                     // 编译错误!需要显式转换
var b int = int(a)                // OK

深入原理:Go 1.9 引入类型别名是为了渐进式代码迁移——允许将类型从一个包移到另一个包而不破坏调用方。rune 和 byte 本身就是类型别名的典型用例(rune = int32, byte = uint8)。


6. 零值

Go 中所有变量声明后都有确定的值,没有 null/undefined/uninitialized:

var i int           // 0
var f float64       // 0.0
var s string        // ""
var b bool          // false
var p *int          // nil
var sl []int        // nil
var m map[int]int   // nil
var ch chan int     // nil
var fn func()       // nil
var in interface{}  // nil

最佳实践:零值机制让你能在声明后立即使用变量而不必显式初始化。善用零值语义(如用 int 零值=0 做计数器),减少样板代码。结构体的零值是所有字段都为零值。


7. 类型转换

Go 没有隐式类型转换,所有转换都必须显式:

var i int = 42
var f float64 = float64(i)        // 显式转换
var u uint = uint(f)

// 数值与字符串互转
import "strconv"

s := strconv.Itoa(42)             // int → string ("42")
n, err := strconv.Atoi("42")      // string → int (42, nil)
f, err := strconv.ParseFloat("3.14", 64)  // string → float64
b, err := strconv.ParseBool("true")       // string → bool

字符串与 []byte / []rune

s := "hello"
bs := []byte(s)                   // string → []byte
rs := []rune(s)                   // string → []rune(按 Unicode 拆分)
s2 := string(bs)                  // []byte → string
s3 := string(rs)                  // []rune → string

注意:string([]byte) 转换会复制底层数据(Go 中 string 不可变),频繁转换会有性能开销。在热路径中,有时用 unsafe.Pointer 避免复制——但必须确保字节切片不会被修改。


8. 字符串

解释字符串 vs 原始字符串

Go 有两种字符串字面量,差别在于是否处理转义序列:

esc := "line1\nline2\t中文"     // 解释字符串:\n \t 等转义生效
raw := `line1\nline2`           // 原始字符串(反引号):所见即所得,\n 就是两个字符

fmt.Println(len(esc))           // 18(\n、\t 各占 1 字节,中文各 3 字节,实测)
fmt.Println(len(raw))           // 12(反斜杠+n 本身被计为 2 个字符,实测)

原始字符串内部不能出现反引号本身,但可以直接换行、不需要对 \ 做任何转义——写正则、嵌入 JSON 模板、内联脚本时首选,省掉成对的 \\ 噪音:

re := `^\d{3}-\d{4}$`           // 不用写成 "^\\d{3}-\\d{4}$"

byte 与 rune:两种"字符"视角

Go 字符串底层是 UTF-8 字节序列。索引拿到的是字节(byte/uint8),一个非 ASCII 字符会占 1~4 个字节;想要 Unicode 码点,用 rune(int32 别名):

s := "Go语言"
b := s[0]                // byte(uint8)  'G'
r := []rune(s)[2]        // rune(int32)  '语'  ——按码点定位,前两个 ASCII 各占 1 个码点
fmt.Printf("%T %c / %T %c\n", b, b, r, r)  // uint8 G / int32 语(实测)

for i, r := range s 会自动按 UTF-8 解码,i 是字节下标、r 是 rune——这是遍历"字符"的正确姿势(详见第 10 节陷阱)。

基本操作

s := "Hello, 世界"               // Go 源码和字符串都是 UTF-8

// 长度(字节数,不是字符数!)
len(s)                            // 13(每个中文字符 3 字节)

// 字符数
import "unicode/utf8"
utf8.RuneCountInString(s)         // 9

// 遍历字符(不是字节)
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)  // r 是 rune 类型
}

// 索引(按字节)
s[0]                              // 'H' (byte)
s[7]                              // 字节,不完整的中文字符!不要这么做

// 字符串不可变
s[0] = 'h'                        // 编译错误!cannot assign to s[0]

字符串拼接

s1 := "hello"
s2 := "world"

// 方式一:+ 运算符(少量拼接用)
s := s1 + ", " + s2

// 方式二:fmt.Sprintf(需要格式化时)
s := fmt.Sprintf("%s, %s", s1, s2)

// 方式三:strings.Builder(大量拼接,性能最优)
var builder strings.Builder
builder.WriteString(s1)
builder.WriteString(", ")
builder.WriteString(s2)
s := builder.String()

// 方式四:strings.Join(拼接切片)
s := strings.Join([]string{s1, s2}, ", ")

性能提示:循环中大量拼接字符串时,务必使用 strings.Builder(Go 1.10+)或 bytes.Buffer。直接 + 拼接会产生大量临时字符串和内存分配。strings.Builder 内部使用 unsafe.Pointer 避免字节复制。

常用 strings 函数

函数 说明 示例
strings.Contains(s, sub) 是否包含子串 Contains("abc", "b") → true
strings.HasPrefix(s, pre) 是否有前缀 HasPrefix("abc", "a") → true
strings.HasSuffix(s, suf) 是否有后缀 HasSuffix("abc", "c") → true
strings.Index(s, sub) 首次出现位置 Index("abc", "b") → 1
strings.Cut(s, sep) 按首个 sep 切成两段 Cut("a.b.c", ".") → "a", "b.c", true(实测)
strings.CutLast(s, sep) 按最后 sep 切成两段(Go 1.27) CutLast("a.b.c", ".") → "a.b", "c", true(实测)
strings.Replace(s, old, new, n) 替换 n 次(-1=全部) Replace("aaa", "a", "b", 2) → "bba"
strings.ToLower(s) 转小写 ToLower("ABC") → "abc"
strings.ToUpper(s) 转大写 ToUpper("abc") → "ABC"
strings.Trim(s, cut) 去头尾字符 Trim(" abc ", " ") → "abc"
strings.Split(s, sep) 分割 Split("a,b", ",") → ["a", "b"]
strings.Join(parts, sep) 拼接 Join(["a","b"], ",") → "a,b"
strings.Repeat(s, n) 重复 n 次 Repeat("Go", 3) → "GoGoGo"
strings.Clone(s) 显式复制(Go 1.18+) 去共享底层数组

注意:Cut/CutLast 返回三个值 (before, after string, found bool),多值调用必须完整解构,不能直接塞进 fmt.Println 这类单值上下文(实测编译错误)。分隔符没找到时 before 为整个字符串、found 为 false。


9. 格式化输出(fmt 包核心动词)

动词 说明 适用类型
%v 默认格式(万能) 所有类型
%+v 默认格式 + 结构体字段名 struct
%#v Go 语法表示 所有类型
%T 类型 所有类型
%d 十进制整数 int
%x / %X 十六进制(小/大写) int, string, []byte
%b 二进制 int
%o 八进制 int
%f 浮点数 float
%e / %E 科学计数 float
%s 字符串 string, []byte
%q 带引号字符串 string, rune
%p 指针地址 pointer
%t bool 值 bool
%w 包装 error(仅 fmt.Errorf) error
%% 字面百分号 —
fmt.Printf("type=%T, value=%v\n", x, x)
fmt.Printf("struct with names: %+v\n", myStruct)
fmt.Printf("source code: %#v\n", myStruct)

%w 与错误包装

%w 只在 fmt.Errorf 里有意义,把被包装的 error 挂进错误链,供 errors.Is/errors.As 顺藤摸瓜。自 Go 1.20 起支持一条格式串里出现多个 %w,构成多分支错误树(见 07-错误处理.md):

multi := fmt.Errorf("batch failed: %w / %w", errA, errB)  // 合法(实测)
// errors.Is(multi, errA) 与 errors.Is(multi, errB) 均为 true

旧手册"一条 Errorf 只能用一个 %w"的说法已作废(Go 1.20 起即可多 %w)。另外 go vet(Go 1.27 起)会对 %w 传 *E 而 E 本身实现 error 的情形告警——包指针通常破坏 errors.Is/类型断言,见 11-最佳实践与陷阱.md。


10. 常见陷阱

10.1 len(string) 返回字节数,不是字符数

s := "你好"
fmt.Println(len(s))              // 6(每个中文字符 3 字节)
fmt.Println(utf8.RuneCountInString(s))  // 2(这才是字符数)

// 遍历字符串请用 for range,它会正确解码 UTF-8
for _, r := range s {
    fmt.Printf("%c", r)           // 你 好
}

10.2 字符串索引返回的是 byte,不是 rune

s := "你好"
fmt.Printf("%c\n", s[0])          // ä(乱码!只是 "你" 的第一个 UTF-8 字节)
fmt.Printf("%c\n", []rune(s)[0])  // 你(正确)

10.3 iota 在不同 const 块中重置

const (
    A = iota                      // 0
    B                             // 1
)

const (
    C = iota                      // 0(重置了!)
)

10.4 := 在嵌套作用域创建新变量

var err error
if true {
    result, err := doSomething()  // 这是新的 err!外部 err 没被赋值
}
// 外部 err 仍然是 nil!

10.5 int 不是 64 位

// 32 位机器上 int = int32,64 位机器上 int = int64
// 需要明确位数的场景(如二进制协议、文件格式)用 int64/uint32 等
var timestamp int64 = time.Now().Unix()

10.6 float 比较

a := 0.1
b := 0.2
c := 0.3
fmt.Println(a+b == c)             // false! 浮点精度问题
fmt.Println(math.Abs(a+b-c) < 1e-9)  // true(正确比较方式)

10.7 min/max 不是"兜底",NaN 会传染

v := min(math.NaN(), 1.0)         // NaN,不是 1.0(实测)

数据源可能产生 NaN 时(如除法未防护),先用 math.IsNaN 判断,再进 min/max。

10.8 clear 不支持数组,也不把 map 置 nil

m := map[string]int{"a": 1}
clear(m)
fmt.Println(m == nil)             // false(map 还在,只是空,实测)
var arr [3]int
clear(arr)                        // 编译错误:argument must be (or constrained by) map or slice(实测)

想"清空数组",用 arr = [3]int{} 整体赋零值即可。


实战建议

  • 指针带初值一律 new(expr)(Go 1.26+),别再写 t := X; &t 两行样板;存量代码里 x := 30; p := &x 也完全合法,new(T) 旧用法没有被废弃。
  • 取小取大用内建 min/max,多参数直接链式;但注意浮点 NaN 传染,别拿它做兜底钳位。
  • 重置 map/slice 用 clear,比 for k := range m { delete(m, k) } 快且意图清晰;slice 元素含指针时 clear 还能提前断开引用利于 GC。
  • 数字字面量用 0b/0o/0x 前缀 + _ 分隔(Go 1.13+),协议常量、位掩码的可读性提升明显。
  • 正则与模板字符串用反引号原始字符串,省掉成串 \\。
  • 涉及中文的用户可见字符串,长度、截取一律走 []rune 或 utf8.RuneCountInString,只有协议层才按 []byte 处理。
  • 错误包装放心用多个 %w(Go 1.20+),见 07-错误处理.md。