类型不是文档,是关卡
一句话总结
变量的类型会在边界上拦住错误的值,会悄悄地转换能够转换的值,会不声不响地丢弃声明中没有的东西。必须把这三者区分清楚。
为什么必须写类型
不写类型,配置也能运行。所以着急的时候很容易漏掉,漏掉了,当天也不会有什么事。问题出在错误的值传进来的那一天。
没有类型的话,这个值会直接穿过变量边界。穿过去的值经过 locals、经过函数,一直流到资源参数,最终在某个地方炸掉。那时的消息不是“你给的值是错的”,而是“这个函数调用失败了”。必须从配置文件顶部开始往回追踪这个值,才能找到原因。
如果写了类型,同样的错误就会这样被拦住。
Error: Invalid value for input variable
on v.tfvars line 1:
1: tight = 7
number required.
会给出行号,会给出期望的是什么。修复所花的时间完全不同。
工作原理
类型分为几类:基本类型(string·number·bool)、集合类型(list·set·map)、结构类型(object·tuple),以及什么都能接收的 any。
拦住。遇到无法转换的值,当场拒绝。在数字的位置给了列表,元组的个数不对,或者对象缺少必需的属性,连计划都生成不了。
转换。能转换的就转换。在 tfvars 中写 replicas = "3",number 变量里得到的也是 3,debug = "true" 同样会变成真。更悄无声息的是集合的内部。
variable "tags" { type = map(string) }
# tags = { team = "core", rev = 3, tls = true }
# 결과 { "team" = "core", "rev" = "3", "tls" = "true" }
数字 3 变成了字符串“3”,却没有任何人告知。如果以后要用这个值做算术或相等比较,到那时就会出怪事。
丢弃。结构类型还有另一种特性。对象只接收所声明的属性,声明中没有的属性不会报错,而是被丢弃。属性名称写错的话,这个值就会消失,看上去就像应用了默认值。这是类型检查无法发现的典型漏洞。
列表和集合的区别也值得记住。即使给出同样的 ["b", "a", "b"],列表会按顺序保存三个,而集合会丢弃重复,按自己的规则排序,只保存两个。长度不同,所以统计个数的代码会受到影响。
可选属性可以让变量接收更简短的输入。
variable "service" {
type = object({
name = string
port = optional(number, 8080)
tls = optional(bool, false)
})
}
这样设置后,使用方只需给出 name,其余的由默认值补齐。如果不给默认值,这个属性就会变成 null,下游还得再确认,所以尽量把默认值也一起写上。
“不接受 null”的声明有两种表现。有默认值时,传入的 null 会被默认值替换;没有默认值时,则是错误。这是一种把“没有值时会变成什么”钉死在一处的机制。
最后,类型检查与值的检查是两回事。类型看形状,值的范围由 validation 块来看。端口是不是数字,类型看得出来,但这个数字是否在 1024 以上,类型就不知道了。
在现场相遇的样子
最常见的,是把模块输入开放成 any。本意是让使用方方便,结果真正用错的人,看到的却是模块内部的错误。模块边界上的类型既是文档也是契约,所以写得窄一些,对所有人都更好。
第二种是不知道映射内部发生的转换就去使用。把数字放进标签映射,之后用这个值做比较,结果永远是假。如果是要当数字用的值,就把映射的元素类型声明为数字,或者在使用的地方经过转换函数。
第三种是对象的拼写错误。属性名写错了一个,应用却成功了,只有那个值按默认值运行。评审时也不容易看出来。要减少这类问题,养成把实际传入的值通过输出导出来确认一遍的习惯,会有帮助。
下一项实验要做什么
走完八个步骤。确认基本类型中什么会被拦住、什么会被转换;通过输出查看集合内部悄悄发生的转换;比较同样的输入分别用列表、集合、元组接收时有何不同。接着分别确认对象的缺失与多余;用可选属性和默认值接收简短的输入;查看“不接受 null”声明的两种表现。最后两步,把不写类型和窄类型时错误出在哪里并排比较,并用嵌套类型钉死输入规格,确认评分器传入的错误输入会被拒绝。