从 C++ 到 Go:变量声明、常量、基本类型与类型系统的核心差异
一、简述
Go 的类型系统比 C++ 更简单、更严格:没有隐式类型转换,没有函数重载,没有模板/泛型(Go 1.18 后有泛型但用法不同)。这看似"退步",实际上大幅降低了代码的心智负担——编译器替你做了大量约束。
对于 C++ 开发者,最需要适应的三个变化:
- 所有未使用的变量都会报错(
unused variable) - 类型转换必须显式(没有
int x = 3.14这种事) string是值类型(赋值和传参都会拷贝,不像std::string有引用计数)
二、C++ 对照速查表
| 概念 | C++ | Go | 备注 |
|---|---|---|---|
| 变量声明 | int x; | var x int | Go 声明与赋值分离 |
| 类型推断 | auto x = 42; | x := 42 | := = 声明 + 推断 + 赋值 |
| 常量 | const int X = 10; | const X = 10 | Go 常量不需要写类型 |
| 编译期常量 | constexpr int X = 10; | const X = 10 | Go 的 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 int | Go 叫"类型定义",不是别名 |
| 打印 | 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++ 对应 |
|---|---|---|---|
int | 32 或 64(平台相关) | 平台相关 | int 或 long |
int8 | 8 | -128 ~ 127 | int8_t |
int16 | 16 | -32768 ~ 32767 | int16_t |
int32 | 32 | 约 +/- 21 亿 | int32_t |
int64 | 64 | 非常大 | int64_t |
uint | 平台相关 | 非负 | unsigned int |
uint8 | 8 | 0 ~ 255 | uint8_t / unsigned char |
uint16 | 16 | 0 ~ 65535 | uint16_t |
uint32 | 32 | 0 ~ 42 亿 | uint32_t |
uint64 | 64 | 非常大(无负数) | 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 类型 | 大小 | 零值 | 说明 |
|---|---|---|---|
int | 32/64 bit | 0 | 依赖平台 |
float64 | 64 bit | 0 | 默认浮点类型 |
bool | 1 byte | false | |
string | 16 bytes (header) | "" | 值类型,不可变 |
byte | 1 byte | 0 | uint8 别名 |
rune | 4 bytes | 0 | int32 别名,Unicode 码点 |
[]byte | 24 bytes (header) | nil | 切片为 nil |
map | 8 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做数值与字符串互转
豫公网安备41108202000982号