Go 常量和变量

admin
admin 2020年03月07日
  • 在其它设备中阅读本文章

一、有名常量和无名常量

各种 基本数据类型 字面表示形式 (除了 false 和 true)都属于 无名常量 (unnamed constant),或者叫 字面常量 (literal constant)。false 和 true 是预声明的两个有名常量。
在 Go 中,有些值的类型是不确定的。换句话说,有些值的类型有很多可能性。这些值称为 类型不确定值 。对于大多数类型不确定值来说,它们各自都有一个默认类型,除了预声明的 nil。nil 是没有默认类型的。与类型不确定值相对应的概念称为 类型确定值 。无名常量都属于类型不确定值。事实上,Go 中大多数的类型不确定值都属于字面常量和有名常量。
一个字面常量的默认类型取决于它的字面表示形式:

  • 一个字符串字面值的默认类型是预声明的 string 类型。
  • 一个布尔字面值的默认类型是预声明的 bool 类型。
  • 一个整数型字面值的默认类型是预声明的 int 类型。
  • 一个 rune 字面值的默认类型是预声明的 rune(亦即 int32)类型。
  • 一个浮点数字面值的默认类型是预声明的 float64 类型。
  • 如果一个字面值含有虚部字面形式,则此字面值的默认类型是预声明的 complex128 类型。

二、类型不确定常量的显式类型转换

和很多语言一样,Go 也支持类型转换。一个显式类型转换的形式为 T(v),其表示将一个值 v 转换为类型 T。编译器将 T(v)的转换结果视为一个类型为 T 的类型确定值。当然,对于一个特定的类型 T,T(v)并非对任意的值 v 都合法。

对于一个类型不确定常量值 v,有两种情形显式转换 T(v)是合法的:

  • v 可以表示为 T 类型的一个值。转换结果为一个类型为 T 的类型确定常量值。
  • v 的默认类型是一个整数类型(int 或者 rune)并且 T 是一个字符串类型。转换 T(v)将 v 看作是一个 Unicode 码点。转换结果为一个类型为 T 的字符串常量。此字符串常量只包含一个 Unicode 码点,并且可以看作是此 Unicode 码点的 UTF- 8 表示形式。对于不在合法的 Unicode 码点取值范围内的整数 v,转换结果等同于字符串字面值 "\uFFFD"(亦即 "\xef\xbf\xbd")。

事实上,第二种情形并不要求 v 必须是一个常量。如果 v 是一个常量,则转换结果也是一个常量。如果 v 不是一个常量,则转换结果也不是一个常量。
一些合法的转换例子:

// 结果为complex128类型的1.0+0.0i。虚部被舍入了。
complex128(1 + -1e-1000i)
// 结果为float32类型的0.5。这里也舍入了。
float32(0.49999999)
// 只要目标类型不是整数类型,舍入都是允许的。
float32(17000000000000000)
float32(123)
uint(1.0)
int8(-123)
int16(6+0i)
complex128(789)
string(65)          // "A"
string('A')         // "A"
string('\u68ee')    // "森"
string(-1)          // "\uFFFD"
string(0xFFFD)      // "\uFFFD"
string(0x2FFFFFFFF) // "\uFFFD"

下面是一些非法的转换:

int(1.23)     // 1.23不能被表示为int类型值。
uint8(-1)     // -1不能被表示为uint8类型值。
float64(1+2i) // 1+2i不能被表示为float64类型值。
// -1e+1000不能被表示为float64类型值。不允许溢出。
float64(-1e1000)
// 0x10000000000000000做为int值将溢出。
int(0x10000000000000000)
// 字面值65.0的默认类型是float64(不是一个整数类型)。
string(65.0)
// 66+0i的默认类型是complex128(不是一个整数类型)。
string(66+0i)

Go 支持类型推断(type deduction or type inference)。 类型推断是指在某些场合下,程序员可以在代码中使用一些类型不确定值, 编译器会自动推断出这些类型不确定值在特定情景下应被视为某些特定类型的值。在 Go 代码中,如果某处需要一个特定类型的值并且一个类型不确定值可以表示为此特定类型的值, 则此类型不确定值可以使用在此处。Go 编译器将此类型不确定值视为此特定类型的类型确定值。 这种情形常常出现在运算符运算、函数调用和赋值语句中。有些场景对某些类型不确定值并没有特定的类型要求。 在这种情况下,Go 编译器将这些类型不确定值视为类型为它们各自的默认类型的类型确定值。

三、常量声明(constant declaration)

1、类型不确定有名常量

和无名字面常量一样,有名常量也必须都是布尔、数字或者字符串值。 在 Go 中,关键字 const 用来声明有名常量。 下面是一些常量声明的例子。

package main

// 声明了两个单独的有名常量。(是的,
// 非ASCII字符可以用做标识符。)
const π = 3.1416
const Pi = π // 等价于:Pi == 3.1416

// 声明了一组有名常量。
const (
    No         = !Yes
    Yes        = true
    MaxDegrees = 360
    Unit       = "弧度"
)

func main() {
    // 声明了三个局部有名常量。
    const DoublePi, HalfPi, Unit2 = π * 2, π * 0.5, "度"
}

上面每行含有一个等号 = 的语句称为一个常量描述(constant specification)。 每个 const 关键字对应一个常量声明。一个常量声明中可以有若干个常量描述。 上面的例子中含有 4 个常量声明。除了第 3 个,其它的常量声明中都各自只有一个常量描述。 第 3 个常量声明中有 4 个常量描述。
常量声明中的等号 = 表示“绑定”而非“赋值”。 每个常量描述将一个或多个字面量绑定到各自对应的有名常量上。 或者说,每个有名常量其实代表着一个字面常量。
注意,常量可以直接声明在包中,也可以声明在函数体中。 声明在函数体中的常量称为局部常量(local constant),直接声明在包中的常量称为包级常量(package-level constant)。 包级常量也常常被称为全局常量。

2、类型确定有名常量

我们可以在声明一些常量的时候指定这些常量的确切类型。 这样声明的常量称为 类型确定有名常量。 在下面这个例子中,所有这 4 个声明的常量都是类型确定的。 X 和 Y 的类型都是 float32, A 和 B 的类型都是 int64。

const X float32 = 3.14

const (
    A, B int64   = -3, 5
    Y    float32 = 2.718
)

如果一个常量描述中包含多个类型确定常量,则这些常量的类型必然是一样的, 比如上例中的 A 和 B。

我们也可以使用显式类型转换来声明类型确定常量。 下面的例子和上面的例子是完全等价的。

const X = float32(3.14)

const (
    A, B = int64(-3), int64(5)
    Y    = float32(2.718)
)

3、常量声明中的自动补全

在一个包含多个常量描述的常量声明中,除了第一个常量描述,其它后续的常量描述都可以只有标识符部分。 Go 编译器将通过照抄前面最紧挨的一个完整的常量描述来自动补全不完整的常量描述。 比如,在编译阶段,编译器会将下面的代码

const (
    X float32 = 3.14
    Y                // 这里必须只有一个标识符
    Z                // 这里必须只有一个标识符

    A, B = "Go", "language"
    C, _
    // 上一行中的空标识符是必需的(如果
    // 上一行是一个不完整的常量描述)。
)

自动补全为

const (
    X float32 = 3.14
    Y float32 = 3.14
    Z float32 = 3.14

    A, B = "Go", "language"
    C, _ = "Go", "language"
)

4、在常量声明中使用 iota

iota 是 Go 中预声明(内置)的一个特殊的有名常量。 iota 被预声明为 0,但是它的值在编译阶段并非恒定。 当此预声明的 iota 出现在一个常量声明中的时候,它的值在第 n 个常量描述中的值为 n(从 0 开始)。 所以 iota 只对含有多个常量描述的常量声明有意义

iota 和常量描述自动补全相结合有的时候能够给 Go 编程带来很大便利。 比如,下面是一个使用了这两个特性的例子。

package main

func main() {
    const (
        k = 3 // 在此处,iota == 0

        m float32 = iota + .5 // m float32 = 1 + .5
        n                     // n float32 = 2 + .5

        p = 9             // 在此处,iota == 3
        q = iota * 2      // q = 4 * 2
        _                 // _ = 5 * 2
        r                 // r = 6 * 2
        s, t = iota, iota // s, t = 7, 7
        u, v              // u, v = 8, 8
        _, w              // _, w = 9, 9
    )

    const x = iota // x = 0 (iota == 0)
    const (
        y = iota // y = 0 (iota == 0)
        z        // z = 1
    )

    println(m)             // +1.500000e+000
    println(n)             // +2.500000e+000
    println(q, r)          // 8 12
    println(s, t, u, v, w) // 7 7 8 8 9
    println(x, y, z)       // 0 0 1
}

上面的例子只是展示了一下如何使用 iota。 在实际编程中,我们应该用有意义的方式使用之。比如:

const (
    Failed = iota - 1 // == -1
    Unknown           // == 0
    Succeeded         // == 1
)

const (
    Readable = 1 << iota // == 1
    Writable             // == 2
    Executable           // == 4
)

四、变量声明和赋值操作语句

变量可以被看作是在运行时刻存储在内存中并且可以被更改的有名字的值。所有的变量值都是类型确定值。当声明一个变量的时候,我们必须在代码中给编译器提供足够的信息来让编译器推断出此变量的确切类型。在一个函数体内声明的变量称为局部变量。 在任何函数体内声明的变量称为包级或者全局变量。
一个局部变量被声明之后至少要被有效使用一次,否则编译器将报错。 包级变量无此限制。 如果一个变量总是被当作赋值语句中的目标值,那么我们认为这个变量没有被有效使用过。
Go 语言有两种变量声明形式。一种称为标准形式,另一种称为短声明形式。 短声明形式只能用来声明局部变量。

1、标准变量声明形式

每条标准变量声明形式语句起始于一个 var 关键字。 每个 var 关键字跟随着一个变量名。 每个变量名必须为一个标识符。和常量声明一样,多个同类型的变量可以在一条语句中被声明。
下面是几条完整形式的标准变量声明语句。 这些声明确地指定了被声明的变量的类型和初始值。

var lang, website string = "Go", "https://golang.org"
var announceYear int = 2009

除了标准变量声明形式外,有两种简略的声明方式

  • 省略了变量类型(但仍指定了变量的初始值),这时编译器将根据初始值的字面量形式来推断出变量的类型。
  • 省略了初始值(但仍指定了变量类型),这时编译器将使用变量类型的零值做为变量的初始值。
    如果一个初始值是一个类型确定值,则对应声明的变量的类型将被推断为此初始值的类型; 如果一个初始值是一个类型不确定值,则对应声明的变量的类型将被推断为此初始值的默认类型。注意在这种变种中,同时声明的多个变量的类型可以不一样。

    // 变量 lang 和 dynamic 的类型将被推断为内置类型 string 和 bool。
    var lang, dynamic = "Go", false

    // 变量 website 的类型将被推断为内置类型 string。
    var website = "https://golang.org"

下例展示了几个省略了初始值的标准变量声明。每个声明的变量的初始值为它们各自的类型的零值。

var lang, website string      // 两者都被初始化为空字符串。
var n int                     // 被初始化为0。

和常量声明一样,多个变量可以用一对小括号组团在一起被声明。

var (
    lang, bornYear, compiled     = "Go", 2007, true
    announceAt, releaseAt    int = 2009, 2012
    createdBy, website       string
)

2、短变量声明形式

我们也可以用短变量声明形式来声明一些局部变量,出现在一个短声明左侧的项必须都为纯标识符。

package main

func main() {
    // 变量lang和year都为新声明的变量。
    lang, year := "Go language", 2007

    // 这里,只有变量createdBy是新声明的变量。
    // 变量year已经在上面声明过了,所以这里仅仅
    // 改变了它的值,或者说它被重新声明了。
    year, createdBy := 2009, "Google Research"

    // 这是一个纯赋值语句。
    lang, year = "Go", 2012

    print(lang, "由", createdBy, "发明")
    print("并发布于", year, "年。")
    println()
}

每个短声明语句中必须至少有一个新声明的变量。
短变量声明形式和标准变量声明形式有几个显著的区别:

  • 短声明形式不包含 var 关键字,并且不能指定变量的类型。
  • 短变量声明中的赋值符号必须为:=。
  • 在一个短声明语句的左侧,已经声明过的变量和新声明的变量可以共存。 但在一个标准声明语句中,所有左侧出现在的变量必须都为新声明的变量。

3、赋值语句

一旦一个变量被声明之后,它的值可以被通过 赋值语句 来修改。多个变量可以同时在一条赋值语句中被修改。一个赋值语句等号左边的表达式必须是一个可寻址的值、一个映射元素或者一个空标识符。 常量是不可改变的(不可寻址的),所以常量不能做为目标值出现在纯赋值语句的左边,而只能出现在右边用做源值。 变量既可以出现在纯赋值语句的左边用做目标值,也可以出现在右边用做源值。空标识符也可以出现在纯赋值语句的左边,表示不关心对应的目标值。 空标识符不可被用做源值。
一个包含了很多(合法或者不合法的)纯赋值语句的例子:

const N = 123
var x int
var y, z float32

N = 789 // error: N是一个不可变量
y = N   // ok: N被隐式转换为类型float32
x = y   // error: 类型不匹配
x = N   // ok: N被隐式转换为类型int
y = x   // error: 类型不匹配
z = y   // ok
_ = y   // ok

z, y = y, z               // ok
_, y = y, z               // ok
z, _ = y, z               // ok
_, _ = y, z               // ok
x, y = 69, 1.23           // ok
x, y = y, x               // error: 类型不匹配,未显式声明类型转换
x, y = int(y), float32(x) // ok

Go 不支持某些其它语言中的连等语法。下面的赋值语句在 Go 中是不合法的。

var a, b int
a = b = 123 // 语法错误

4、显式类型转换规则

在 Go 中,两个类型不一样的基本类型值是不能相互赋值的。我们必须使用显式类型转换将一个值转换为另一个值的类型之后才能进行赋值。整数(不论常量还是非常量)都可以被显式转换为字符串类型。

  • 一个非常量浮点数和整数可以显式转换到其它任何一个浮点数和整数类型。
  • 一个非常量复数可以显式转换到其它任何一个复数类型。
    常量数字值的类型转换不能溢出。此规则不适用于非常量数字值的类型转换。 非常量数字值的类型转换中,溢出是允许的。 另外当将一个浮点数非常量值(比如一个变量)转换为一个整数类型的时候,舍入(或者精度丢失)也是允许的。 具体规则如下:
  • 当从一个比特位数多的整数类型的非常量整数值向一个比特位数少的整数类型转换的时候,高位的比特将被舍弃,低位的比特将被保留。 我们称这种处理方式为截断(truncated)或者回绕(wrapped around)。
  • 当从一个非常量的浮点数向一个整数类型转换的时候,浮点数的小数部分将被舍弃(向零靠拢)。
  • 当从一个非常量整数或者浮点数向一个浮点数类型转换的时候,精度丢失是可以发生的。
  • 当从一个非常量复数向另一个复数类型转换的时候,精度丢失也是可以发生的。
  • 当一个显式转换涉及到非常量浮点数或者复数数字值时,如果源值溢出了目标类型的表示范围,则转换结果取决于具体编译器实现(即行为未定义)。
    在下面的例子中,第 7 行和第 15 行的隐式转换是不允许的,第 5 行和第 14 行的显式转换也是不允许的。
    const a = -1.23
    // 变量 b 的类型被推断为内置类型 float64。
    var b = a
    // error: 常量 1.23 不能被截断舍入到一个整数。
    var x = int32(a)
    // error: float64 类型值不能被隐式转换到 int32。
    var y int32 = b
    // ok: z == -1,变量 z 的类型被推断为 int32。
    // z 的小数部分将被舍弃。
    var z = int32(b)

const k int16 = 255
var n = k // 变量 n 的类型将被推断为 int16。
var f = uint8(k + 1) // error: 常量 256 溢出了 uint8。
var g uint8 = n + 1 // error: int16 值不能隐式转换为 uint8。
var h = uint8(n + 1) // ok: h == 0,变量 h 的类型为 uint8。

                 // (n+1)溢出uint8,所以只有低8位
                 // bits(都为0)被保留。

五、变量和常量的作用域和生命周期

作用域 是指一个标识符的可见范围。变量(常量)的生命周期指的是在程序运行期间变量有效存在的时间。在 Go 中,我们可以使用一对大括号来显式形成一个(局部)代码块。一个代码块可以内嵌另一个代码块。最外层的代码块称为包级代码块。一个声明在一个内层代码块中的常量或者变量将遮挡另一个外层代码块中声明的同名变量或者常量。变量的生命周期与变量的作用域有着不可分割的联系。根据变量(常量、类型或函数)定义位置的不同,可以分为以下三个类型:

  • 代码块内定义的变量称为局部变量:生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止
  • 代码块外定义的变量称为全局变量:生命周期和整个程序的运行周期是一致的,直到程序结束
  • 函数定义中的形式参数和函数返回值:只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元