从 C++ 到 Go:变量声明、常量、基本类型与类型系统的核心差异


一、简述

Go 的类型系统比 C++ 更简单、更严格:没有隐式类型转换,没有函数重载,没有模板/泛型(Go 1.18 后有泛型但用法不同)。这看似"退步",实际上大幅降低了代码的心智负担——编译器替你做了大量约束。

对于 C++ 开发者,最需要适应的三个变化:

  1. 所有未使用的变量都会报错(unused variable)
  2. 类型转换必须显式(没有 int x = 3.14 这种事)
  3. string 是值类型(赋值和传参都会拷贝,不像 std::string 有引用计数)

二、C++ 对照速查表

概念C++Go备注
变量声明int x;var x intGo 声明与赋值分离
类型推断auto x = 42;x := 42:= = 声明 + 推断 + 赋值
常量const int X = 10;const X = 10Go 常量不需要写类型
编译期常量constexpr int X = 10;const X = 10Go 的 const 全部是编译期常量
枚举enum Color { RED, GREEN };const ( RED = iota; GREEN )Go 用 iota 实现枚举
隐式转换double d = 42;(合法)不允许Go 必须显式:float64(42)
字符串std::string s = "hi";(引用语义)s := "hi"(值类型)Go string 赋值是拷贝
字符char c = 'A';var c byte = 'A' 或 var c rune = 'A'Go 没有 char 类型
多变量声明int a, b;var a, b int或 a, b := 1, 2
零值未初始化(UB/随机值)自动零值Go 所有变量声明后都有零值
类型别名using MyInt = int;type MyInt intGo 叫"类型定义",不是别名
打印std::cout << x;fmt.Println(x)Go 用 fmt 包格式化输出

三、变量声明

3.1 var 声明

package main

import "fmt"

func main() {
    // 基本语法:var 变量名 类型
    var name string          // 声明但不赋值,零值为 ""
    var age int              // 零值为 0
    var active bool          // 零值为 false

    // 声明并赋值
    var score int = 100
    var lang string = "Go"

    // 类型推断:省略类型,编译器从右侧推断
    var version = 2          // 推断为 int
    var pi = 3.14            // 推断为 float64(Go 默认浮点类型)
    var greeting = "hello"   // 推断为 string

    fmt.Println(name, age, active, score, lang, version, pi, greeting)
}

C++ 对照:

// C++
std::string name;           // 默认构造,空字符串
int age = 0;                // 未初始化 = 随机值(UB)
auto version = 2;           // == Go 的 var version = 2

3.2 := 短变量声明(最常用)

func main() {
    // := 只能在函数内部使用(不能用于包级别变量)
    x := 42              // x 是 int
    name := "Go"         // name 是 string
    pi := 3.14           // pi 是 float64

    // 多变量同时声明
    a, b := 1, "hello"   // a 是 int, b 是 string

    fmt.Println(x, name, pi, a, b)
}

关键规则:

  • := 必须至少有一个新变量(左边至少有一个没声明过的)
  • := 只能在函数体内使用,包级别用 var
func main() {
    x := 1
    // x := 2              // 编译错误!x 已经声明了
    x, y := 2, 3          // 合法,y 是新变量
    fmt.Println(x, y)
}

// 包级别
var globalVal = 100        // 正确
// globalVal2 := 200       // 编译错误!:= 不能用在包级别

3.3 多变量声明

// var 块声明(常用于包级别或一组相关变量)
var (
    host    string = "localhost"
    port    int    = 8080
    timeout int    = 30
)

// 简洁多赋值
var x, y, z int = 1, 2, 3
a, b, c := "hello", 42, true   // 类型可以不同

四、常量与 iota

4.1 基本常量

const Pi = 3.14159265358979    // 无类型常量(更灵活)
const MaxRetries int = 3        // 有类型常量

// const 也可以用块
const (
    AppName    = "MyApp"
    AppVersion = "1.0.0"
)

// C++ 对照
// constexpr double Pi = 3.14159265358979;
// const int MaxRetries = 3;

Go 常量的特殊之处:

  • 常量值必须在编译期确定(不能 const x = someFunc())
  • 无类型常量可以参与更多运算(比如 const huge = 1 << 100,赋值给 int 之前不会溢出)

4.2 iota 枚举(Go 独有)

// iota 在 const 块中自动递增,从 0 开始
const (
    Sunday = iota    // 0
    Monday           // 1(省略的值自动用 iota)
    Tuesday          // 2
    Wednesday        // 3
    Thursday         // 4
    Friday           // 5
    Saturday         // 6
)

// C++ 对照:
// enum Weekday { Sunday = 0, Monday = 1, Tuesday = 2, ... };
// 或用 enum class + 手动编号

iota 的高级用法:

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

// 自定义枚举配合位运算
const (
    FlagRead  = 1 << iota  // 1 (二进制 001)
    FlagWrite = 1 << iota  // 2 (二进制 010)
    FlagExec  = 1 << iota  // 4 (二进制 100)
)

// 使用
flags := FlagRead | FlagWrite  // flags = 3

五、基本类型

5.1 整数类型

类型位宽范围C++ 对应
int32 或 64(平台相关)平台相关int 或 long
int88-128 ~ 127int8_t
int1616-32768 ~ 32767int16_t
int3232约 +/- 21 亿int32_t
int6464非常大int64_t
uint平台相关非负unsigned int
uint880 ~ 255uint8_t / unsigned char
uint16160 ~ 65535uint16_t
uint32320 ~ 42 亿uint32_t
uint6464非常大(无负数)uint64_t
uintptr平台相关用于存放指针无直接对应
func main() {
    // 整数字面量
    var a int = 42           // 十进制
    var b int = 0xFF         // 十六进制(255)
    var c int = 077          // 八进制(63)
    var d int = 0b1010       // 二进制(Go 1.13+)

    // 可以用下划线分隔(Go 1.13+)
    var million = 1_000_000  // 等同于 1000000

    fmt.Println(a, b, c, d, million)
}

5.2 浮点类型

func main() {
    var f32 float32 = 3.14   // C++ 的 float
    var f64 float64 = 3.14   // C++ 的 double(Go 默认浮点类型)

    // Go 默认推断为 float64
    pi := 3.14               // float64

    // 科学记数法
    speed := 3.0e8           // float64,光速(米/秒)

    fmt.Println(f32, f64, pi, speed)
}

5.3 布尔、字符串、字符

func main() {
    // bool
    var ok bool = true
    done := false

    // string — 值类型!赋值是拷贝
    s1 := "hello"
    s2 := s1     // s2 是 s1 的拷贝,修改 s2 不影响 s1
    s3 := "hello, 世界"  // 支持 UTF-8

    // byte = uint8(单个字节)
    var ch byte = 'A'       // ASCII 字符用 byte

    // rune = int32(一个 Unicode 码点)
    var zh rune = '中'      // 中文字符用 rune
    var emoji rune = '😀'   // emoji 也行

    // string 的底层是 []byte,但不能直接索引为 rune
    s := "Hello, 世界"
    fmt.Println(len(s))       // 13(字节数,不是字符数)
    fmt.Println([]rune(s))    // [72 101 108 108 111 44 32 19990 30028](rune 切片)

    fmt.Println(ok, done, s1, s2, s3, ch, zh, emoji)
}

C++ 对照:

// C++
char ch = 'A';                // 1 字节
const char* s = "hello";      // C 风格字符串
std::string s = "Hello, 世界"; // UTF-8 但 std::string 无感知
wchar_t wc = L'中';           // 宽字符

// Go 没有 char。单字节用 byte,Unicode 码点用 rune。

5.4 类型总结表

Go 类型大小零值说明
int32/64 bit0依赖平台
float6464 bit0默认浮点类型
bool1 bytefalse
string16 bytes (header)""值类型,不可变
byte1 byte0uint8 别名
rune4 bytes0int32 别名,Unicode 码点
[]byte24 bytes (header)nil切片为 nil
map8 bytes (header)nil未初始化的 map 是 nil
指针8 bytes (64bit)nil

六、零值(Zero Value)

Go 中所有变量声明后都有确定的零值,不存在"未初始化"的概念。这是 Go 与 C++ 最大的差异之一。

func main() {
    // 所有变量自动初始化为零值
    var i int            // 0
    var f float64        // 0
    var b bool           // false
    var s string         // ""
    var p *int           // nil
    var sl []int         // nil
    var m map[string]int // nil

    fmt.Printf("int:    %d\n", i)    // 0
    fmt.Printf("float:  %f\n", f)    // 0.000000
    fmt.Printf("bool:   %t\n", b)    // false
    fmt.Printf("string: %q\n", s)    // ""
    fmt.Printf("ptr:    %v\n", p)    // <nil>
    fmt.Printf("slice:  %v\n", sl)   // []
    fmt.Printf("map:    %v\n", m)    // map[]
}

C++ 对照:

// C++:未初始化的局部变量是未定义行为!
int x;           // 随机值(UB)
double d;        // 随机值(UB)
bool flag;       // 随机值(UB)
// Go:永远不会出现这种情况,所有零值都是确定的

七、类型转换

Go 不支持任何隐式类型转换,所有类型转换都必须显式进行。

7.1 基本类型转换

import (
    "fmt"
    "strconv"
)

func main() {
    // 数值之间的转换
    var i int = 42
    var f float64 = float64(i)    // int → float64(显式)
    var u uint = uint(f)          // float64 → uint(截断小数部分)

    // 重新赋值也要显式
    var x int32 = 100
    var y int64 = int64(x)        // 不能隐式转

    // 注意:Go 不做精度检查,可能丢失数据
    var big float64 = 3.99
    var small int = int(big)      // small = 3(截断,不四舍五入)

    // string 与数值互转需要用 strconv 包
    s := strconv.Itoa(42)         // int → string: "42"
    n, err := strconv.Atoi("42")  // string → int: 42, nil
    if err != nil {
        fmt.Println("转换失败:", err)
    }
    // n 的正常使用在块末的 fmt.Println(..., n, ...)

    // string ↔ []byte / []rune
    str := "Hello, 世界"
    bytes := []byte(str)          // string → []byte
    runes := []rune(str)          // string → []rune
    back := string(bytes)         // []byte → string
    back2 := string(runes)        // []rune → string

    fmt.Println(f, u, x, y, s, n, back, back2)
}

C++ 对照:

// C++ 允许隐式转换(可能出 bug)
int i = 3.99;           // i = 3(隐式,可能非预期)
double d = 42;          // d = 42.0(隐式,安全但不显式)
long long ll = 1000000000LL;
int small = ll;          // 可能溢出(隐式)

// Go 强制你写出转换,让意图明确
// var i int = 3.99       // 编译错误!
// var d float64 = 42     // 编译错误!

7.2 自定义类型之间的转换

type Celsius float64
type Fahrenheit float64

func CtoF(c Celsius) Fahrenheit {
    return Fahrenheit(c*9/5 + 32)  // 底层类型相同,可以互相转换
}

func main() {
    var temp Celsius = 100
    fmt.Println(CtoF(temp))  // 212

    // 但不能直接赋值
    // var f Fahrenheit = temp  // 编译错误!Celsius 和 Fahrenheit 不同类型
    var f Fahrenheit = Fahrenheit(temp)  // 必须显式
    fmt.Println(f)
}

八、类型推断规则

:= 的推断遵循以下规则:

func main() {
    // 整数字面量 → int
    x := 42            // int

    // 浮点字面量 → float64(Go 的默认浮点类型)
    pi := 3.14         // float64

    // 复数字面量 → complex128
    c := 1 + 2i        // complex128

    // true/false → bool
    flag := true        // bool

    // 字符串字面量 → string
    name := "Go"       // string

    // rune 字面量 → rune (int32)
    ch := '中'         // rune

    // byte 字面量 → byte (uint8)
    b := byte('A')     // byte

    // nil → 无法推断,必须写类型
    // x := nil           // 编译错误!
    var p *int          // p = nil(必须显式声明类型)

    // 赋值给已有类型的变量不会改变类型
    var num int64 = 42
    num = 100           // 仍然是 int64,不会变成 int

    fmt.Println(x, pi, c, flag, name, ch, b, num)
}

C++ 对照:

// C++ auto 的推断规则类似
auto x = 42;           // int
auto pi = 3.14;        // double(C++ 默认)
auto flag = true;      // bool

// C++ 的 auto 可以配合花括号列表
auto vec = {1, 2, 3};  // std::initializer_list<int>
// Go 没有这种语法

九、常见坑

坑 1::= 作用域陷阱

func main() {
    x := 1
    fmt.Println(x)  // 1

    // if 语句创建新的作用域
    if true {
        x := 2           // 这是新变量,不是修改外面的 x!
        fmt.Println(x)   // 2
    }

    fmt.Println(x)  // 仍然是 1!
}

C++ 对照:C++ 的 if 里声明的变量出了 if 就消失,但不会产生"遮蔽"同名变量的问题——因为 C++ 允许在同一作用域内重新声明。

坑 2:未使用的变量

func main() {
    x := 42
    // y := 10     // 编译错误!y 声明了但未使用
    // Go 严格要求:声明了就必须用,否则报错
    fmt.Println(x)

    // 如果你真的不需要值,用 _ 忽略
    _, err := someFunc()
    if err != nil {
        fmt.Println(err)
    }
}

func someFunc() (int, error) {
    return 0, nil
}

坑 3:float64 精度问题

func main() {
    var a float64 = 0.1
    var b float64 = 0.2
    fmt.Println(a + b)          // 0.30000000000000004(浮点精度问题)

    // 金额等精确计算不要用 float64,用整数或 math/big
    // 正确做法:用最小单位(分)计算
    price1 := 10    // 0.10 元 = 10 分
    price2 := 20    // 0.20 元 = 20 分
    fmt.Println(price1 + price2) // 30 分 = 0.30 元
}

坑 4:字符串是不可变值类型

func main() {
    s := "hello"
    // s[0] = 'H'       // 编译错误!string 不可变
    // 必须转为 []byte 再修改
    b := []byte(s)
    b[0] = 'H'
    s = string(b)
    fmt.Println(s)  // "Hello"

    // 传参是拷贝(但 string header 很小,数据不会复制)
    modify(s)
    fmt.Println(s)  // 仍然是 "Hello"
}

func modify(s string) {
    s = "world"  // 只修改了本地副本
}

坑 5:未初始化的 map

func main() {
    // var m map[string]int      // m 是 nil,不能写入!
    // m["key"] = 1              // panic: assignment to entry in nil map

    // 必须用 make 初始化
    m := make(map[string]int)
    m["key"] = 1
    fmt.Println(m)
}

坑 6:类型转换溢出

func main() {
    var big int32 = 1000
    small := int8(big)    // small = -24(溢出!Go 不会报错或 panic)
    fmt.Println(small)

    // 类似地,浮点转整数会截断
    f := 3.99
    i := int(f)           // i = 3(不是 4,不四舍五入)
    fmt.Println(i)
}

十、练习任务

  • 用三种方式(var、:=、var 块)声明变量并打印它们的值
  • 定义一组 iota 常量表示 HTTP 状态码(200, 301, 404, 500)
  • 尝试将 float64 赋值给 int 变量,观察编译错误,然后用显式转换修复
  • 演示 := 在 if 块中的作用域遮蔽效果
  • 编写一个函数,接收 string 参数并在函数内修改它,验证外部字符串不受影响
  • 用 iota 和位运算实现一组文件权限常量(Read, Write, Exec)
  • 分别打印 int, float64, bool, string, []int, map[string]int 的零值
  • 尝试 strconv.Itoa 和 strconv.Atoi 做数值与字符串互转

上一篇:01-环境与工具链
下一篇:03-流程控制