目录

const

💡 部分内容是观看李文周视频做的笔记,部分是学习极客时间博客专栏做的笔记。

常量定义了之后就不能修改,程序运行期间不能改变。常量一般定义在程序开头,不写在程序尾部。

1. 原始定义

1
const pai = 3.1415926

2. 多个常量同时定义

1
2
3
4
const (
	statusok       = 200
	statusnotfound = 404
)

3. 默认值😀

1
2
3
4
5
6
const (
	n1 = 100
	n2 // 如果后面没有写值,默认和上面一行一样!
	n3
)
fmt.Println(n1, n2, n3)

out

1
100 100 100

上面三个例子可以观察到,常量赋值了,而且必须赋值。是不会有默认值的。比如

1
const a int

这样声明是错误的,Goland或者是其他编辑器会给出提示,并且编译也是过不去的。而且定义常量也是不需要定义类型的,多次一举!

4. iota

iota是go语言的常量计数器,只能在常量的表达式中使用。

iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

  • 每遇到一个关键字const,自动清零
  • 每换行一次,自动加1

4.1 示例1

1
2
3
4
5
const (
	c1 = iota // 默认是0,const出现的时候重置为0,**const中每加1行,iota就加1**
	c2        // = iota
	c3        // = iota
)

out

1
0 1 2

4.2 示例2

1
2
3
4
5
6
const (
	m1 = iota   //0  
	m2 = 100    //iota+1  1
	m3 = iota   //iota+1  2
	m4          // 3
)

out

1
0 100 2 3

4.3 示例3

1
2
3
4
5
6
const (
	m1 = iota   // 0 
	m2 = 100    // 100
	m3          // 100
	m4          // 100
)

输出

1
0 100 100 100

4.4 示例4

1
2
3
4
const (
	q1, q2 = iota + 1, iota + 2   // 1 2
	q3, q4 = iota + 1, iota + 2   // 2,3  一般不这么用,只有面试或者刷算法才会这么做
)

输出

1
1 2 2 3

5. 匿名变量与iota

当有些数据必须用变量接收,但又不使用它时,就可以用_来接收这个值。哑元变量

1
2
3
4
5
6
7
const (
	a1 = iota
	a2
	_    // 这是匿名变量,也增加了一行,所以a3=3!
	a3
)
fmt.Println(a1, a2, a3)

输出

1
0 1 3

6. iota定义数量级

1
2
3
4
5
6
7
8
//定义数量级
const (
	_  = iota
	KB = 1 << (10 * iota) // 左移10位,2的10次方,1024  10000000000
	MB = 1 << (10 * iota) // 1MB =1024KB, 移动20位
	GB = 1 << (10 * iota)
	TB = 1 << (10 * iota)
)

输出

1
1024 1048576 1073741824 1099511627776

7. 无类型常量

Go 语言对类型安全是有严格要求的:即便两个类型拥有着相同的底层类型,但它们仍然是不同的数据类型,不可以被相互比较或混在一个表达式中进行运算。这一要求不仅仅适用于变量,也同样适用于有类型常量(Typed Constant)中,你可以在下面代码中看出这一点:

1
2
3
4
5
6
7
8
type myInt int
const n myInt = 13
const m int = n + 5 // 编译器报错:cannot use n + 5 (type myInt) as type int in const initializer

func main() {
    var a int = 5
    fmt.Println(a + n) // 编译器报错:invalid operation: a + n (mismatched types int and myInt)
}

上面这段代码如果是在Goland中编写,那么在没有编译前,Goland就会给你飘红了。

1
2
3
4
5
6
7
8
type myInt int
const n myInt = 13
const m int = int(n) + 5  // OK

func main() {
    var a int = 5
    fmt.Println(a + int(n))  // 输出:18
}

除了进行显式的强制类型转换,还有方法解决这个问题吗?其实在我们第一节的时候,声明不加类型就可以了。看下面的代码

1
2
3
4
5
6
7
type myInt int
const n = 13

func main() {
    var a myInt = 5
    fmt.Println(a + n)  // 输出:18
}

在这个代码中,常量 n 在声明时并没有显式地被赋予类型,在 Go 中,这样的常量就被称为无类型常量(Untyped Constant)。不过,无类型常量也不是说就真的没有类型,它也有自己的默认类型,不过它的默认类型是根据它的初值形式来决定的。像上面代码中的常量 n 的初值为整数形式,所以它的默认类型为 int。

8. 隐式转型😀

隐式转型说的就是,对于无类型常量参与的表达式求值,Go 编译器会根据上下文中的类型信息,把无类型常量自动转换为相应的类型后,再参与求值计算,这一转型动作是隐式进行的。但由于转型的对象是一个常量,所以这并不会引发类型安全问题,Go 编译器会保证这一转型的安全性。

1
2
3
4
const m = 1333333333

var k int8 = 1
j := k + m // 编译器报错:constant 1333333333 overflows int8

可以看到,无类型常量与常量隐式转型的“珠联璧合”使得在 Go 这样的具有强类型系统的语言,在处理表达式混合数据类型运算的时候具有比较大的灵活性,代码编写也得到了一定程度的简化。也就是说,我们不需要在求值表达式中做任何显式。在实际编程中,声明常量时,尽量使用无类型声明,也就是第一章节那样,不需要加数据类型

所以要注意隐式转型的坑!会将常量转型到另外一个已知的数据类型。如果进行计算,可能会出现超出范围等错误。

9. iota进阶

9.1 从1开始接收

想要略过 iota = 0,从 iota = 1 开始正式定义枚举常量,我们可以效仿下面标准库中的代码:

1
2
3
4
5
6
7
// $GOROOT/src/syscall/net_js.go
const (
    _ = iota
    IPV6_V6ONLY  // 1
    SOMAXCONN    // 2
    SO_ERROR     // 3
)

在这个代码里,我们使用了空白标识符作为第一个枚举常量,它的值就是 iota。虽然它本身没有实际意义,但后面的常量值都会重复它的初值表达式(这里是 iota),于是我们真正的枚举常量值就从 1 开始了。

9.2 枚举不连续

如果我们的枚举常量值并不连续,而是要略过某一个或几个值,又要怎么办呢?我们也可以借助空白标识符来实现,如下面这个代码:

1
2
3
4
5
6
7
8
const (
    _ = iota // 0
    Pin1
    Pin2
    Pin3
    _
    Pin5    // 5   
)

可以看到,在上面这个枚举定义中,枚举常量集合中没有 Pin4。为了略过 Pin4,我们在它的位置上使用了空白标识符。这样,Pin5 就会重复 Pin3,也就是向上数首个不为空的常量标识符的值,这里就是 iota,而且由于它所在行的偏移值为 5,因此 Pin5 的值也为 5,这样我们成功略过了 Pin4 这个枚举常量以及 4 这个枚举值。

9.3 枚举更加容易😀

iota 特性让我们维护枚举常量列表变得更加容易。比如我们使用传统的枚举常量声明方式,来声明一组按首字母排序的“颜色”常量,也就是这样:

1
2
3
4
5
const ( 
    Black  = 1 
    Red    = 2
    Yellow = 3
)

假如这个时候,我们要增加一个新颜色 Blue。那根据字母序,这个新常量应该放在 Red 的前面呀。但这样一来,我们就需要像下面代码这样将 Red 到 Yellow 的常量值都手动加 1,十分费力。

1
2
3
4
5
6
const (
    Blue   = 1
    Black  = 2
    Red    = 3
    Yellow = 4
)

那如果我们使用 iota 重新定义这组“颜色”枚举常量是不是可以更方便呢?我们可以像下面代码这样试试看:

1
2
3
4
5
6
const (
    _ = iota     
    Blue  // 没写值默认是iota的值,这一点我以前还没发现
    Red 
    Yellow     
)

这样,无论后期我们需要增加多少种颜色,我们只需将常量名插入到对应位置就可以,其他就不需要再做任何手工调整了。

9.4 独立变化😀

如果一个 Go 源文件中有多个 const 代码块定义的不同枚举,每个 const 代码块中的 iota 也是独立变化的,也就是说,每个 const 代码块都拥有属于自己的 iota,如下面代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const (
    a = iota + 1 // 1, iota = 0
    b            // 2, iota = 1
    c            // 3, iota = 2
)

const (
    i = iota << 1 // 0, iota = 0
    j             // 2, iota = 1
    k             // 4, iota = 2
)

可以看到,每个 iota 的生命周期都始于一个 const 代码块的开始,在该 const 代码块结束时结束。

这里要注意,b和c不是默认的a的值,而是iota+1,这个也是iota的特性之一。

10. 总结

  • 可以不写类型
  • 要赋值

常量是一种在源码编译期间被创建的语法元素,它的值在程序的生命周期内保持不变。所有常量的求值计算都是在编译期完成的,而不是在运行期,这样可以减少运行时的工作,也方便编译器进行编译优化。另外,当操作数是常量表达式时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界等。

Go 语言原生提供了对常量的支持,所以我们可以避免像 C 语言那样,使用宏定义常量,这比较复杂,也容易发生错误。而且,Go 编译器还为我们提供的类型安全的保证。

无类型常量,这是 Go 在常量方面的创新。无类型常量拥有和字面值一样的灵活性,它可以直接参与到表达式求值中,而不需要使用显式地类型转换。这得益于 Go 对常量的另一个创新:隐式转型,也就是将无类型常量的默认类型自动隐式转换为求值上下文中所需要的类型,并且这一过程由 Go 编译器保证安全性,这大大简化了代码编写。

此外,Go 常量还“移植”并改良了前辈 C 语言的枚举类型的特性,在 const 代码块中支持自动重复上一行和 iota 行偏移量指示器。这样我们就可以使用 Go 常量语法来实现枚举常量的定义。并且,基于 Go 常量特性的枚举定义十分灵活,维护起来也更为简便。比如,我们可以选择以任意数值作为枚举值列表的起始值,也可以定义不连续枚举常量,添加和删除有序枚举常量时也不需要手工调整枚举的值。