你不知道的 Go 之 const

Posted darjun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你不知道的 Go 之 const相关的知识,希望对你有一定的参考价值。

简介

常量可以说在每个代码文件中都存在,使用常量有很多好处:

  • 避免魔法字面量,即直接出现在代码中的数字,字符串等。阅读代码的时候无法一眼看出它的含义。另外可以避免使用字面量可能出现的不一致,当它们的值需要修改时,常量只需修改一处,而字面量要修改多处,容易遗漏造成不一致;
  • 相对于变量,常量可以执行编译期优化。

Go 语言也提供了常量的语法支持,与其他语言提供的常量基本一致。但是 Go 中的常量有几个有用的特性值得了解一下。

常量基础

Go 语言中使用const关键字定义常量:

package main

import "fmt"

const PI float64 = 3.1415926
const MaxAge int = 150
const Greeting string = "hello world"

func main() {
  fmt.Println(PI)
  fmt.Println(MaxAge)
  fmt.Println(Greeting)
}

多个常量定义可以合并在一起,如上面的几个常量定义可以写成下面的形式:

const (
  PI       float64 = 3.1415926
  MaxAge   int     = 150
  Greeting string  = "hello world"
)

不过通常建议将相同类型的,相关联的常量定义在一个组里面。

Go 语言中常量有一个很大的限制:只能定义基本类型的常量,即布尔类型(bool),整数(无符号uint/uint8/uint16/uint32/uint64/uintptr,有符号int/int8/int16/int32/int64),浮点数(单精度float32,双精度float64),或者底层类型是这些基本类型的类型。不能定义切片,数组,指针,结构体等这些类型的常量。例如,byte底层类型为uint8rune底层类型为int32,见 Go 源码builtin.go

// src/builtin/builtin.go
type byte = uint8
type rune = int32

故可以定义类为byterune的常量:

const b byte = 128
const r rune = \'c\'

定义其他类型的变量会在编译期报错:

type User struct {
  Name string
  Age  int
}

const u User = User{} // invalid const type User

var i int = 1
const p *int = &i // invalid const type *int

iota

Go 语言的代码中常量定义经常使用iota,下面看几个 Go 的源码。

标准库time源码:

// src/time/time.go
type Month int

const (
  January Month = 1 + iota
  February
  March
  April
  May
  June
  July
  August
  September
  October
  November
  December
)

type Weekday int

const (
  Sunday Weekday = iota
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
)

标准库net/http源码:

// src/net/http/server.go
type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

iota是方便我们定义常量的一个机制。简单来说,iota独立作用于每一个常量定义组中(单独出现的每个const语句都算作一个组),iota出现在用于初始化常量值的常量表达式中,iota的值为它在常量组中的第几行(从 0 开始)。使用iota定义的常量下面可以省略类型和初始化表达式,这时会沿用上一个定义的类型和初始化表达式。我们看几组例子:

const (
  One int = iota + 1
  Two
  Three
  Four
  Five
)

这个也是最常使用的方式,iota出现在第几行,它的值就是多少。上面常量定义组中,One在第 0 行(注意从 0 开始计数),iota为 0,所以One = 0 + 1 = 1
下一行Two省略了类型和初始化表达式,因此Two沿用上面的类型int,初始化表达式也是iota + 1。但是此时是定义组中的第 1 行,iota的值为 1,所以Two = 1 + 1 = 2
再下一行Three也省略了类型和初始化表达式,因此Three沿用了Two进而沿用了One的类型int,初始化表达式也是iota + 1,但是此时是定义的第 2 行,所以Three = 2 + 1 = 3。以此类推。

我们可以在非常复杂的初始化表达式中使用iota

const (
  Mask1 int = 1<<(iota+1) - 1
  Mask2
  Mask3
  Mask4
)

按照上面的分析Mask1~4依次为 1, 3, 7, 15。

另外还有奇数,偶数:

const (
  Odd1 = 2*iota + 1
  Odd2
  Odd3
)

const (
  Even1 = 2 * (iota + 1)
  Even2
  Even3
)

在一个组中,iota不一定出现在第 0 行,但是它出现在第几行,值就为多少:

const (
  A int = 1
  B int = 2
  C int = iota + 1
  D
  E
)

上面iota出现在第 2 行(从 0 开始),C的值为2 + 1 = 3DE分别为 4, 5。

一定要注意iota的值等于它出现在组中的第几行,而非它的第几次出现。

可以通过赋值给空标识符来忽略值:

const (
  _ int = iota
  A // 1
  B // 2
  C // 3
  D // 4
  E // 5
)

说了这么多iota的用法,那么为什么要用iota呢?换句话说,iota有什么优点?我觉得有两点:

  • 方便定义,在模式比较固定的时候,我们可以只写出第一个,后面的常量不需要写出类型和初始化表达式了;
  • 方便调整顺序,增加和删除常量定义,如果我们定义了一组常量之后,想要调整顺序,使用iota的定义,只需要调整位置即可,不需要修改初始化式,因为就没有写。增加和删除也是一样的,如果我们一个个写出了初始化式,删除中间某个,后续的值就必须做调整。

例如,net/http中的源码:

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosed
)

如果我们需要增加一个常量,表示正在关闭的状态。现在只需要写出新增的状态名:

type ConnState int

const (
  StateNew ConnState = iota
  StateActive
  StateIdle
  StateHijacked
  StateClosing // 新增的状态
  StateClosed
)

如果是显式写出初始化式:

type ConnState int

const (
  StateNew ConnState = 0
  StateActive ConnState = 1
  StateIdle ConnState = 2
  StateHijacked ConnState = 3
  StateClosed ConnState = 4
)

这时新增需要改动后续的值。另外需要键入的字符也多了不少

以上是关于你不知道的 Go 之 const的主要内容,如果未能解决你的问题,请参考以下文章

你不知道的 Go 之 slice

Go 语言 10 岁了!这里有你不知道的 Go 的成长历程

Go 语言也 10 岁了!这里或许有你不知道的 Go 的成长历程

函数作用域和块级作用域--你不知道的JavaScript

你知道的Go切片扩容机制可能是错的

Go基础之iota