CodeGym /课程 /Go SELF /Go 中的常量:有类型与无类型

Go 中的常量:有类型与无类型

Go SELF
第 8 级, 课程 1
可用

1. 初识 const

到目前为止,我们主要使用的是变量——也就是在程序执行过程中可以修改的值:

var age int = 18

变量会存在、会变化、会依赖用户输入、程序逻辑和条件。今天我们要认识一个新词——const。它表示常量:一个只设置一次、之后永远不再改变的值。

例如:

const maxUsers = 100

之后就不能再给 maxUsers 重新赋值了。如果你尝试写 maxUsers = 200,程序根本无法编译。 这并不是“为了严格而严格”,而是一种有意识的保护:如果从语义上看值不应该改变,那最好在语言层面把它固定下来。

还要理解另一件事:常量必须在编译期间已知。 也就是说,它的值不能来自 fmt.Scan、文件或者网络。例如,下面这样写是不行的:

var x int
fmt.Scan(&x)

const y = x // 错误

编译器在运行时并不知道 x 会是多少,因此它不能把它变成常量。 常量讲的是规则和固定值,而不是程序的当前状态。

常量经常用于“硬边界”和配置——也就是从语义上不应该变化的东西:

const minAge = 18

if age >= minAge {
    fmt.Println("welcome")
}

这里 minAge 就是系统规则。如果它是普通变量,就可能被意外修改,程序逻辑也会变得不那么可预测。 常量则把意图说得很明确:这是一个固定值。

常量组

在 Go 中,常量常常不是单独声明,而是成组声明——通过 const ( ... )。 这只是把“规则”放在一起的一种方便方式:边界、限制、消息字符串、模式代码。 它的含义并没有改变:块中的每一行都是普通常量,只是写得更紧凑。

const (
	minAge   = 18
	maxUsers = 100
	appName  = "DoorGuard"
)

这样的块可以读成一个小小的“配置表”:你能看出这些是固定值,而且它们放在一起。 至于其他方面,规则还是一样——常量不能修改,而且必须能在编译期计算出来。

你也许会觉得 const 只是 “var,不过不能改值而已”。 但接下来你会发现,Go 中的常量其实更有意思。 它们有一个特点:可以是 typed 或 untyped,正是这一点解释了为什么常量有时会比变量更灵活。

2. Untyped 常量与默认类型

如果你刚刚适应“类型就是规则”这个想法,Go 会突然来个小变招: int 不能和 int64 直接相加,但你可以写 time.Second / 1e3 或者 math.Sqrt(2)—— 编译器并不会要求你对 int64(2) 和 float64(2) 手忙脚乱地转换。 这不是魔法,也不是“对新手放水”,而是一个有意识的设计: Go 中的常量生活在一个和变量稍微不同的世界里。

为了在这个世界里不迷路,我们需要掌握两个术语:

  • untyped 常量——没有固定 Go 类型的常量;
  • typed 常量——有明确指定类型的常量(例如 const x int = 5)。

由于这种差异,常量的“行为”和变量不一样:有时会“适应上下文”,有时又会倔强地要求显式转换。

Untyped:有值,没有类型

当你写:

const n = 5

直觉上你会想说:“这不就是 int 吗?” 但更准确的理解是:这是数字 5,只是暂时不必是 int/int64/uint。 它“还没有身份证”,可以稍后再拿到——这取决于你把它放到哪里使用。

字符串字面量也是一样。字符串字面量 "Hello, 世界" 并不等同于“一个 string 类型的值”, 至少不完全等同于变量 var s string 的那种意义。 它是一个 untyped string constant,可以被赋给任何期望字符串类型的位置,而不会造成类型冲突。

迷你示例:一个常量适配不同类型

package main

import "fmt"

func main() {
	const n = 5 // untyped

	var a int = n
	var b int64 = n

	fmt.Printf("a=%v (%T)\n", a, a) // a=5 (int)
	fmt.Printf("b=%v (%T)\n", b, b) // b=5 (int64)
}

核心思想是:类型属于变量(a 和 b), 而不是常量。只要值可表示(能放进去而且不失去意义),常量就会“嵌入”到所需的类型里。

类型上下文与默认类型

在某个时刻,编译器总要做决定:如果你没有指定类型,该选哪一个? 对于 untyped 常量,有一个概念叫 default type(默认类型): 当没有其他类型信息时,它就会“显现”出来。

例如:

str := "Hello, 世界"

它的效果就像你写了 var str string = "Hello, 世界"。 也就是说,当没有更多提示时,untyped 常量会“提示”变量的类型。

表:默认类型

代码中的字面量/常量 示例 默认类型(如果没有其他信息)
整数
const x = 10
int
浮点数
const x = 1.5
float64
字符串
const s = "go"
string
布尔值
const b = true
bool
rune(单引号中的字符)
const r = 'A'
rune(即 int32)

入门阶段只要记住一条就够了:整数 → int,小数 → float64, 只要上下文没有要求别的类型。

细节:为什么 %T 显示 int

如果你写:

fmt.Printf("%T\n", 5)

你会看到 int。但这并不意味着字面量 5 在内部永远都是 int。 这表示在当前上下文中(参数传递),Go 需要形成一个具体值—— 于是就使用了 default type。

小例子:

package main

import "fmt"

func main() {
	const n = 5      // untyped
	const f = 1.25   // untyped
	const s = "task" // untyped

	fmt.Printf("%T\n", n) // int
	fmt.Printf("%T\n", f) // float64
	fmt.Printf("%T\n", s) // string
}

fmt.Printf 实际上会迫使常量“进入值的世界”,而它会带着默认类型出现。

3. Typed 常量与类似变量的规则

再来看另一种写法:

const m int = 5

这就是 typed 常量。它有一个具体类型(int), 并且它遵守的规则几乎和这种类型的普通值一样: 如果你想把它当成 int64 使用,编译器就会要求你进行显式转换。

迷你示例:untyped 可以“默默适配”,typed 则必须显式转换

package main

import "fmt"

func main() {
	const n = 5     // untyped
	const m int = 5 // typed

	var a int64 = n        // ok
	var b int64 = int64(m) // 需要转换

	fmt.Println(a, b) // 5 5
}

为什么这么严格?因为 Go 原则上不会自己混合类型, 而 typed 常量几乎就像变量一样——只是不能改值。

表达式中的 Untyped:会“贴”到邻居类型上

这常常会让新手感到惊讶:为什么 int64Var + 5 能工作, 而 int64Var + typedIntConst 却不行。

意思是:表达式中的 untyped 常量会尽量变成表达式所需要的类型,只要这可行。 Typed 常量已经“选边站”了,因此需要显式协调。

package main

import "fmt"

func main() {
	var x int64 = 10

	const untyped = 5
	const typed int = 5

	fmt.Println(x + untyped)      // 15
	// fmt.Println(x + typed)     // 无法编译
	fmt.Println(x + int64(typed)) // 15
}

这里的逻辑很清楚:如果值已经是 typed(int),那么在同一个表达式里就不能不经过程序员明确决定而把 int 和 int64 混用。

4. 可表示性与范围检查

在选择类型之前进行常量计算

有一个很舒服的细节:数值型 untyped 常量生活在“任意精度”的数字空间里, 编译器可以像数学那样对它们进行计算,直到结果必须装进某个具体类型为止。

可以这样记:

  • “只要还是常量”——就可以相当自由地计算。
  • “一旦要放进变量里”——类型限制就生效:范围、符号、精度。

示例:

package main

import "fmt"

func main() {
	const big = 1000

	var x uint8 = big // 无法编译:1000 放不进 uint8
	fmt.Println(big) // 1000
}

编译器提前就知道:uint8 的范围是 0…255。所以这个赋值不可能成功。 这很好:错误会立即被捕获,而不是在运行时某个地方才爆出来。

范围检查:编译器作为早期保护

不要把常量和变量的行为混在一起。对于变量,有时可以“强行”做转换(并得到溢出); 但对于常量,编译器经常会禁止那些看起来可能毫无意义的操作。

最简单的范围检查:

package main

import "fmt"

func main() {
	const ok = 255
	// const bad = 256

	var b uint8 = ok
	fmt.Println(b) // 255
}

如果你尝试使用一个在目标类型中无法表示的值,编译器会立刻拦住你。

5. 实践:迷你 CLI 与用常量做校验

继续我们“迷你控制台应用”的学习路线。设想我们要输入任务的“优先级”,范围从 1 到 5。

这里的目标很简单:看看哪里适合使用 untyped 常量, 哪里又应该使用 typed 常量(这样编译器就能帮我们避免出错)。

把 untyped 常量当作“不会妨碍别人”的规则

package main

import "fmt"

func main() {
	const minPriority = 1
	const maxPriority = 5

	var p int
	fmt.Scan(&p)

	ok := p >= minPriority && p <= maxPriority
	fmt.Println(ok) // 例如:true
}

minPriority 和 maxPriority 很适合保持为 untyped: 它们可以很好地与 int 比较,而且你不会引入多余的转换。

Typed 常量作为小类型的契约

有时你事先就想把数字放进一个小类型里,例如 uint8。 这种情况下,typed 常量 就明确了意图:“这是一个 byte,就这么定了”。

package main

import "fmt"

func main() {
	const maxLen uint8 = 40

	var title string
	fmt.Scan(&title)

	fmt.Println(len(title) <= int(maxLen)) // len(title) 是 int
}

是的,这里还是会出现 int(maxLen), 因为 len 返回的是 int。 但更重要的是:maxLen 是 uint8——这是一份“契约”。 如果你试图把它设成 300,编译器不会允许。

6. 示意图:untyped 常量的类型如何被选择

有时把这张简单的路线图放在脑子里,会很有帮助,这样就不会猜“为什么这里行得通,那里却不行”。

flowchart TD
    A[我们有一个常量: const x = 5] --> B{有显式类型吗?}
    B -- yes --> C[Typed constant: x 有类型]
    B -- no --> D[Untyped constant: 没有类型]
    D --> E{上下文要求具体类型吗?}
    E -- yes --> F[如果值可表示,就适配所需类型]
    E -- no --> G[使用默认类型: int/float64/string/...]
    C --> H[像变量一样: 需要 cast 到其他类型]
    F --> I[如果不可表示: compile error]

关键词是:“上下文”和“可表示”(也就是值确实能放进目标类型里并保持有意义)。

7. 典型错误

错误 1:“我打印了 %T 并看到了 int,所以常量永远都是 int。”
这种想法通常出现在第一次做 fmt.Printf 实验之后。 实际上,untyped 常量只有在上下文要求时才会获得类型,而通过 Printf 打印恰好就创造了这种上下文。 所以 %T 显示的不是“常量的真实本质”,而是程序某个具体点上得到的值的类型。

错误 2:“既然是常量,那就应该尽可能严格:把所有东西都显式类型化。”
这会导致相反的问题:你开始在本来只是想固定 “5” 和 “60” 的地方,到处写 int64(x) 和 float64(y)。 untyped 常量的意义,正是让你在显而易见的地方不必做转换,同时仍然保留变量的严格类型系统。

错误 3:“为什么 int64Var + 5 可以,而 int64Var + constTypedInt 不行?编译器坏了吗?”
编译器没有坏:5 是 untyped,可以“贴”到 int64 上, 而 typed 常量 int 已经是具体类型了,不能在没有显式决定的情况下与 int64 混用。 这和变量遵循的是同一个原则:Go 不喜欢替程序员猜测。

错误 4:“我就写 const max uint8 = 300,以后再修。”
不行:编译器不会让你通过。常量会在编译期检查是否可表示,这也是它们的超能力之一: 你会立刻发现错误,而不用等到奇怪的溢出出现。

错误 5:“既然常量这么灵活,那我可以用它们替代用户输入。”
常量不能依赖 fmt.Scan,也不能依赖程序执行的结果。 如果值来自外部(输入、文件、网络),那它一定是变量。 常量讲的是规则和固定值,而且这些值在编译时就已知。

1
任务
Go SELF, 第 8 级, 课程 1
已锁定
常量护照
常量护照
1
任务
Go SELF, 第 8 级, 课程 1
已锁定
加五
加五
1
任务
Go SELF, 第 8 级, 课程 1
已锁定
门禁检查
门禁检查
1
任务
Go SELF, 第 8 级, 课程 1
已锁定
磁盘与块
磁盘与块
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION