Go語言 常量和變量 - 順便介紹了類型不確定值和類型推斷

2023-02-16 17:36 更新

這篇文章將介紹常量和變量相關的知識。 類型不確定值、類型推斷和值的顯式類型轉換等概念也將被介紹。

上一章中提到的基本類型的字面量表示 (除了falsetrue)都屬于無名常量(unnamed constant),或者叫字面常量(literal constant)。 falsetrue是預聲明的兩個具名常量。本文將介紹如何聲明自定義的具名常量。

類型不確定值(untyped value)和類型確定值(typed value)

在Go中,有些值的類型是不確定的。換句話說,有些值的類型有很多可能性。 這些值稱為類型不確定值。對于大多數(shù)類型不確定值來說,它們各自都有一個默認類型, 除了預聲明的nil。nil是沒有默認類型的。 我們在后續(xù)的文章中將了解到很多關于nil的知識。

與類型不確定值相對應的概念稱為類型確定值。

上一章提到的字面常量(無名常量)都屬于類型不確定值。 事實上,Go中大多數(shù)的類型不確定值都屬于字面常量和本文即將介紹的具名常量。 少數(shù)類型不確定值包括剛提到的nil和以后會逐步接觸到的某些操作的布爾返回值。

一個字面(常)量的默認類型取決于它為何種字面量形式:

  • 一個字符串字面量的默認類型是預聲明的string類型。
  • 一個布爾字面量的默認類型是預聲明的bool類型。
  • 一個整數(shù)型字面量的默認類型是預聲明的int類型。
  • 一個rune字面量的默認類型是預聲明的rune(亦即int32)類型。
  • 一個浮點數(shù)字面量的默認類型是預聲明的float64類型。
  • 如果一個字面量含有虛部字面量,則此字面量的默認類型是預聲明的complex128類型。

類型不確定常量的顯式類型轉換

和很多語言一樣,Go也支持類型轉換。 一個顯式類型轉換的形式為T(v),其表示將一個值v轉換為類型T。 編譯器將T(v)的轉換結果視為一個類型為T的類型確定值。 當然,對于一個特定的類型T,T(v)并非對任意的值v都合法。

下面介紹的規(guī)則同時適用于上一章介紹的字面常量和即將介紹的類型不確定具名常量。

對于一個類型不確定常量值v,有兩種情形顯式轉換T(v)是合法的:

  1. v可以表示為T類型的一個值。 轉換結果為一個類型為T的類型確定常量值。
  2. v的默認類型是一個整數(shù)類型(int或者rune) 并且T是一個字符串類型。 轉換T(v)v看作是一個Unicode碼點。 轉換結果為一個類型為T的字符串常量。 此字符串常量只包含一個Unicode碼點,并且可以看作是此Unicode碼點的UTF-8表示形式。 對于不在合法的Unicode碼點取值范圍內的整數(shù)v, 轉換結果等同于字符串字面量"\uFFFD"(亦即"\xef\xbf\xbd")。 0xFFFD是Unicode標準中的(非法碼點的)替換字符值。 (但是請注意,今后的Go版本可能只允許rune或者byte整數(shù)被轉換為字符串。 從Go官方工具鏈1.15版本開始,go vet命令會對從非rune和非byte整數(shù)到字符串的轉換做出警告。)

事實上,第二種情形并不要求v必須是一個常量。 如果v是一個常量,則轉換結果也是一個常量。 如果v不是一個常量,則轉換結果也不是一個常量。

一些合法的轉換例子:

// 結果為complex128類型的1.0+0.0i。虛部被舍入了。
complex128(1 + -1e-1000i)
// 結果為float32類型的0.5。這里也舍入了。
float32(0.49999999)
// 只要目標類型不是整數(shù)類型,舍入都是允許的。
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(不是一個整數(shù)類型)。
string(65.0)
// 66+0i的默認類型是complex128(不是一個整數(shù)類型)。
string(66+0i)

從上面的例子可以看出,一個類型不確定數(shù)字值所表示的值可能溢出它的默認類型的表示范圍。 比如上例中的-1e10000x10000000000000000。 一個溢出了它的默認類型的表示范圍的類型不確定數(shù)字值是不能被轉換到它的默認類型的(將編譯報錯)。

注意,有時一個顯式轉換形式必須被寫成(T)(v)以免發(fā)生歧義。 這種情況多發(fā)生在T不為一個標識符的時候。

我們以后將在其它章節(jié)學到更多的顯式類型轉換規(guī)則。

類型推斷介紹

Go支持類型推斷(type deduction or type inference)。 類型推斷是指在某些場合下,程序員可以在代碼中使用一些類型不確定值, 編譯器會自動推斷出這些類型不確定值在特定情景下應被視為某些特定類型的值。

在Go代碼中,如果某處需要一個特定類型的值并且一個類型不確定值可以表示為此特定類型的值, 則此類型不確定值可以使用在此處。Go編譯器將此類型不確定值視為此特定類型的類型確定值。 這種情形常常出現(xiàn)在運算符運算、函數(shù)調用和賦值語句中。

有些場景對某些類型不確定值并沒有特定的類型要求。在這種情況下,Go編譯器將這些類型不確定值視為它們各自的默認類型的類型確定值。

上述兩條類型推斷規(guī)則可以被視為隱式轉換規(guī)則。

本文下面的章節(jié)將展示一些類型推斷的例子。 后續(xù)其它文章將會展示更多類型推斷的例子和規(guī)則。

(具名)常量聲明(constant declaration)

和無名字面常量一樣,具名常量也必須都是布爾、數(shù)字或者字符串值。 在Go中,關鍵字const用來聲明具名常量。 下面是一些常量聲明的例子。

package main

// 聲明了兩個單獨的具名常量。(是的,
// 非ASCII字符可以用做標識符。)
const π = 3.1416
const Pi = π // 等價于:const Pi = 3.1416

// 聲明了一組具名常量。
const (
	No         = !Yes
	Yes        = true
	MaxDegrees = 360
	Unit       = "弧度"
)

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

Go白皮書把上面每行含有一個等號=的語句稱為一個常量描述(constant specification)。 每個const關鍵字對應一個常量聲明。一個常量聲明中可以有若干個常量描述。 上面的例子中含有4個常量聲明。除了第3個,其它的常量聲明中都各自只有一個常量描述。 第3個常量聲明中有4個常量描述。

在上面的例子中,符號*是一個乘法運算符, 符號!是一個布爾取否運算符。 運算符將在下一篇文章中詳述。

常量聲明中的等號=表示“綁定”而非“賦值”。 每個常量描述將一個或多個字面量綁定到各自對應的具名常量上。 或者說,每個具名常量其實代表著一個字面常量。

在上面的例子中,具名常量πPi都綁定到(或者說代表著)字面常量3.1416。 這兩個具名常量可以在程序代碼中被多次使用,從而有效避免了字面常量3.1416在代碼中出現(xiàn)在多處。 如果字面常量3.1416在代碼中出現(xiàn)在多處, 當我們以后欲將3.1416改為3.14的時候,所有出現(xiàn)在代碼中的3.1416都得逐個修改。 有了具名常量的幫助,我們只需修改對應常量描述中的3.1416即可。 這是常量聲明的主要作用。當然常量聲明也可常常增加代碼的可讀性(代碼即注釋)。

以后,我們使用非常量這一術語表示不是常量的值。 下一節(jié)將要介紹的變量就屬于非常量。

注意,常量可以直接聲明在包中,也可以聲明在函數(shù)體中。 聲明在函數(shù)體中的常量稱為局部常量(local constant),直接聲明在包中的常量稱為包級常量(package-level constant)。 包級常量也常常被稱為全局常量。

包級常量聲明中的常量描述的順序并不重要。比如在上面的例子中, 常量描述NoYes的順序可以掉換一下。

上面例子中聲明的所有常量都是類型不確定的。 它們各自的默認類型和它們各自代表的字面量的默認類型是一樣的。

類型確定具名常量

我們可以在聲明一些常量的時候指定這些常量的確切類型。 這樣聲明的常量稱為類型確定具名常量。 在下面這個例子中,所有這4個聲明的常量都是類型確定的。 XY的類型都是float32, AB的類型都是int64。

const X float32 = 3.14

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

如果一個常量描述中包含多個類型確定常量,則這些常量的類型必然是一樣的, 比如上例中的AB。

我們也可以使用顯式類型轉換來聲明類型確定常量。 下面的例子和上面的例子是完全等價的。

const X = float32(3.14)

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

欲將一個字面常量綁定到一個類型確定具名常量上,此字面常量必須能夠表示為此常量的確定類型的值。 否則,編譯將報錯。

const a uint8 = 256             // error: 256溢出uint8
const b = uint8(255) + uint8(1) // error: 256溢出uint8
const c = int8(-128) / int8(-1) // error: 128溢出int8
const MaxUint_a = uint(^0)      // error: -1溢出uint
const MaxUint_b uint = ^0       // error: -1溢出uint

在上面的例子中,符號^為位反運算符,符號+為加法運算符,符號/為除法運算符。

下面這個類型確定常量聲明在64位的操作系統(tǒng)上是合法的,但在32位的操作系統(tǒng)上是非法的。 因為一個uint值在32位操作系統(tǒng)上的尺寸是32位, (1 << 64) - 1將溢出uint。(這里,符號<<為左移位運算符。)

const MaxUint uint = (1 << 64) - 1

那么如何聲明一個代表著最大uint值的常量呢? 我們可以用下面這個常量聲明來替換上面這個。下面這個聲明在64位和32位的操作系統(tǒng)上都是合法的。

const MaxUint = ^uint(0)

類似地,我們可以使用下面這個常量聲明來聲明一個具名常量來表示最大的int值。(這里,符號>>為右移位運算符。)

const MaxInt = int(^uint(0) >> 1)

使用類似的方法,我們可以聲明一個常量來表示當前操作系統(tǒng)的位數(shù),或者檢查當前操作系統(tǒng)是32位的還是64位的。

const NativeWordBits = 32 << (^uint(0) >> 63) // 64 or 32
const Is64bitOS = ^uint(0) >> 63 != 0
const Is32bitOS = ^uint(0) >> 32 == 0

這里,符號!===分別為不等于和等于比較運算符。

常量聲明中的自動補全

在一個包含多個常量描述的常量聲明中,除了第一個常量描述,其它后續(xù)的常量描述都可以只包含標識符列表部分。 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"
)

在常量聲明中使用iota

iota是Go中預聲明(內置)的一個特殊的具名常量。 iota被預聲明為0,但是它的值在編譯階段并非恒定。 當此預聲明的iota出現(xiàn)在一個常量聲明中的時候,它的值在第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
)

在上面這段代碼中,-是一個減法運算符。

變量聲明和賦值操作語句

變量可以被看作是在運行時刻存儲在內存中并且可以被更改的具名的值。

所有的變量值都是類型確定值。當聲明一個變量的時候,我們必須在代碼中給編譯器提供足夠的信息來讓編譯器推斷出此變量的確切類型。

在一個函數(shù)體內聲明的變量稱為局部變量。 在任何函數(shù)體外聲明的變量稱為包級或者全局變量。

Go語言有兩種變量聲明形式。一種稱為標準形式,另一種稱為短聲明形式。 短聲明形式只能用來聲明局部變量。

標準變量聲明形式

每條標準變量聲明形式語句起始于一個var關鍵字。 每個var關鍵字跟隨著一個變量名。 每個變量名必須為一個標識符。

下面是幾條完整形式的標準變量聲明語句。 這些聲明確地指定了被聲明的變量的類型和初始值。

var lang, website string = "Go", "https://golang.org"
var compiled, dynamic bool = true, false
var announceYear int = 2009

我們可以看到,和常量聲明一樣,多個同類型的變量可以在一條語句中被聲明。

完整形式的標準變量聲明使用起來有些羅嗦,因此很少在日常Go編程中使用。 在日常Go編程中,另外兩種變種形式用得更廣泛一些。 一種變種形式省略了變量類型(但仍指定了變量的初始值),這時編譯器將根據(jù)初始值的字面量形式來推斷出變量的類型。 另一種變種形式省略了初始值(但仍指定了變量類型),這時編譯器將使用變量類型的零值做為變量的初始值。

下面是一些第一種變種形式的用例。在這些用例中,如果一個初始值是一個類型確定值,則對應聲明的變量的類型將被推斷為此初始值的類型; 如果一個初始值是一個類型不確定值,則對應聲明的變量的類型將被推斷為此初始值的默認類型。 注意在這種變種中,同時聲明的多個變量的類型可以不一樣。

// 變量lang和dynamic的類型將被推斷為內置類型string和bool。
var lang, dynamic = "Go", false

// 變量compiled和announceYear的類型將被推斷
// 為內置類型bool和int。
var compiled, announceYear = true, 2009

// 變量website的類型將被推斷為內置類型string。
var website = "https://golang.org"

上例中的類型推斷可以被視為隱式類型轉換。

下例展示了幾個省略了初始值的標準變量聲明。每個聲明的變量的初始值為它們各自的類型的零值。 

var lang, website string      // 兩者都被初始化為空字符串。
var interpreted, dynamic bool // 兩者都被初始化為false。
var n int                     // 被初始化為0。

和常量聲明一樣,多個變量可以用一對小括號組團在一起被聲明。

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

上面這個變量聲明語句已經(jīng)被go fmt命令格式化過了。 這個變量聲明語句包含三個變量描述(variable specification)。

一般來說,將多個相關的變量聲明在一起將增強代碼的可讀性。

純賦值語句

在上面展示的變量聲明的例子中,等號=表示賦值。 一旦一個變量被聲明之后,它的值可以被通過純賦值語句來修改。 多個變量可以同時在一條賦值語句中被修改。

一個賦值語句等號左邊的表達式必須是一個可尋址的值、一個映射元素或者一個空標識符。 內存地址(以及指針)和映射將在以后的文章中介紹。

常量是不可改變的(不可尋址的),所以常量不能做為目標值出現(xiàn)在純賦值語句的左邊,而只能出現(xiàn)在右邊用做源值。 變量既可以出現(xiàn)在純賦值語句的左邊用做目標值,也可以出現(xiàn)在右邊用做源值。

空標識符也可以出現(xiàn)在純賦值語句的左邊,表示不關心對應的目標值。 空標識符不可被用做源值。

一個包含了很多(合法或者不合法的)純賦值語句的例子:

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

上例中的最后一行使用了顯式類型轉換,否則此賦值(見倒數(shù)第二行)將不合法。 數(shù)字非常量值的類型轉換規(guī)則將在后邊的章節(jié)介紹。

Go不支持某些其它語言中的連等語法。下面的賦值語句在Go中是不合法的。

var a, b int
a = b = 123 // 語法錯誤

短變量聲明形式

我們也可以用短變量聲明形式來聲明一些局部變量。比如下例:

package main

func main() {
	// 變量lang和year都為新聲明的變量。
	lang, year := "Go language", 2007

	// 這里,只有變量createdBy是新聲明的變量。
	// 變量year已經(jīng)在上面聲明過了,所以這里僅僅
	// 改變了它的值,或者說它被重新聲明了。
	year, createdBy := 2009, "Google Research"

	// 這是一個純賦值語句。
	lang, year = "Go", 2012

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

每個短聲明語句中必須至少有一個新聲明的變量。

從上面的例子中,我們可以看到短變量聲明形式和標準變量聲明形式有幾個顯著的區(qū)別:

  1. 短聲明形式不包含?var?關鍵字,并且不能指定變量的類型。
  2. 短變量聲明中的賦值符號必須為?:=?。
  3. 在一個短聲明語句的左側,已經(jīng)聲明過的變量和新聲明的變量可以共存。 但在一個標準聲明語句中,所有出現(xiàn)在左側的變量必須都為新聲明的變量。

注意,相對于純賦值語句,目前短聲明語句有一個限制:出現(xiàn)在一個短聲明左側的項必須都為純標識符。 以后我們將學習到在純賦值語句的左邊可以出現(xiàn)結構體值的字段,指針的解引用和容器類型值的元素索引項等。 但是這些項不能出現(xiàn)在一個變量短聲明語句的左邊。

關于“賦值”這個術語

以后,當“賦值”這個術語被提到的時候,它可以指一個純賦值、一個短變量聲明或者一個初始值未省略的標準變量聲明。 事實上,一個更通用的定義包括后續(xù)文章將要介紹的函數(shù)傳參。

y = x是一條合法的賦值語句時,我們可以說x可以被賦給y。 假設y的類型為Ty,有時為了敘述方便,我們也可以說x可以被賦給類型Ty。

一般來說,如果x可以被賦給y,則y應該是可修改的,并且xy的類型相同或者x可以被隱式轉換到y的類型。 當然,y也可以是空標識符_

每個局部聲明的變量至少要被有效使用一次

注意,當使用目前的主流Go編譯器編譯Go代碼時,一個局部變量被聲明之后至少要被有效使用一次,否則編譯器將報錯。 包級變量無此限制。 如果一個變量總是被當作賦值語句中的目標值,那么我們認為這個變量沒有被有效使用過。

下面這個例子編譯不通過。

package main

var x, y, z = 123, true, "foo" // 包級變量

func main() {
	var q, r = 789, false
	r, s := true, "bar"
	r = y // r沒有被有效使用。
	x = q // q被有效使用了。
}

當編譯上面這個程序的時候,編譯器將報錯(這個程序代碼存在一個名為example-unused.go的文件中):

./example-unused.go:6:6: r declared and not used
./example-unused.go:7:16: s declared and not used

避免編譯器報錯的方法很簡單,要么刪除相關的變量聲明,要么像下面這樣,將未曾有效使用過的變量(這里是rs)賦給空標識符。

package main

var x, y, z = 123, true, "foo"

func main() {
	var q, r = 789, false
	r, s := true, "bar"
	r = y
	x = q

	_, _ = r, s // 將r和s做為源值使用一次。
}

若干包級變量在聲明時刻的依賴關系將影響它們的初始化順序

下面這個例子中的聲明的變量的初始化順序為y = 5、c = yb = c+1、a = b+1x = a+1。

var x, y = a+1, 5         // 8 5
var a, b, c = b+1, c+1, y // 7 6 5

包級變量在初始化的時候不能相互依賴。比如,下面這個變量聲明語句編譯不通過。

var x, y = y, x

值的可尋址性

在Go中,有些值是可以被尋址的。上面已經(jīng)提到所有變量都是可以尋址的,所有常量都是不可被尋址。 我們可以從后面的指針一文了解更多關于內存地址和指針的知識。

非常量數(shù)字值相關的顯式類型轉換規(guī)則

在Go中,兩個類型不一樣的基本類型值是不能相互賦值的。 我們必須使用顯式類型轉換將一個值轉換為另一個值的類型之后才能進行賦值。

前面某節(jié)已經(jīng)提到了整數(shù)(不論常量還是非常量)都可以被顯式轉換為字符串類型。 這里再介紹兩個不同類型數(shù)字值之間的轉換規(guī)則。

  • 一個非常量浮點數(shù)和整數(shù)可以顯式轉換到其它任何一個浮點數(shù)和整數(shù)類型。
  • 一個非常量復數(shù)可以顯式轉換到其它任何一個復數(shù)類型。

上面已經(jīng)提到,常量數(shù)字值的類型轉換不能溢出。此規(guī)則不適用于非常量數(shù)字值的類型轉換。 非常量數(shù)字值的類型轉換中,溢出是允許的。 另外當將一個浮點數(shù)非常量值(比如一個變量)轉換為一個整數(shù)類型的時候,舍入(或者精度丟失)也是允許的。 具體規(guī)則如下:

  • 當從一個比特位數(shù)多的整數(shù)類型的非常量整數(shù)值向一個比特位數(shù)少的整數(shù)類型轉換的時候,高位的比特將被舍棄,低位的比特將被保留。我們稱這種處理方式為截斷(truncated)。
  • 當從一個非常量的浮點數(shù)向一個整數(shù)類型轉換的時候,浮點數(shù)的小數(shù)部分將被舍棄(向零靠攏)。
  • 當從一個非常量整數(shù)或者浮點數(shù)向一個浮點數(shù)類型轉換的時候,精度丟失是可以發(fā)生的。
  • 當從一個非常量復數(shù)向另一個復數(shù)類型轉換的時候,精度丟失也是可以發(fā)生的。
  • 當一個顯式轉換涉及到非常量浮點數(shù)或者復數(shù)數(shù)字值時,如果源值溢出了目標類型的表示范圍,則轉換結果取決于具體編譯器實現(xiàn)(即行為未定義)。

在下面的例子中,第7行和第15行的隱式轉換是不允許的,第5行和第14行的顯式轉換也是不允許的。

const a = -1.23
// 變量b的類型被推斷為內置類型float64。
var b = a
// error: 常量1.23不能被截斷舍入到一個整數(shù)。
var x = int32(a)
// error: float64類型值不能被隱式轉換到int32。
var y int32 = b
// ok: z == -1,變量z的類型被推斷為int32。
//     z的小數(shù)部分將被舍棄。
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)被保留。

3行的隱式轉換中,a被轉換為它的默認類型(float64);因此b的類型被推斷為float64

變量和常量的作用域

在Go中,我們可以使用一對大括號來顯式形成一個(局部)代碼塊。一個代碼塊可以內嵌另一個代碼塊。 最外層的代碼塊稱為包級代碼塊。 一個聲明在一個內層代碼塊中的常量或者變量將遮擋另一個外層代碼塊中聲明的同名變量或者常量。 比如,下面的代碼中聲明了3個名為x的變量。 內層的x將遮擋外層的x, 從而外層的x在內層的x聲明之后在內層中將不可見。

package main

const y = 70
var x int = 123 // 包級變量

func main() {
	// 此x變量遮擋了包級變量x。
	var x = true

	// 一個內嵌代碼塊。
	{
		x, y := x, y-10 // 這里,左邊的x和y均為新聲明
		                // 的變量。右邊的x為外層聲明的
		                // bool變量。右邊的y為包級變量。

		// 在此內層代碼塊中,從此開始,
		// 剛聲明的x和y將遮擋外層聲明x和y。

		x, z := !x, y/10 // z是一個新聲明的變量。
		                 // x和y是上一句中聲明的變量。
		println(x, y, z) // false 60 6
	}
	println(x) // true
	println(y) // 70 (包級變量y從未修改)
	/*
	println(z) // error: z未定義。
	           // z的作用域僅限于上面的最內層代碼塊。
	*/
}

剛提到的作用域是指一個標識符的可見范圍。 一個包級變量或者常量的作用域為其所處于的整個代碼包。 一個局部變量或者常量的作用域開始于此變量或者常量的聲明的下一行,結束于最內層包含此變量或者常量的聲明語句的代碼塊的結尾。 這解釋了為什么上例中的println(z)將編譯不通過。

后面的代碼塊和作用域一文將詳述代碼塊和標識符的作用域。

更多關于常量聲明

一個類型不確定常量所表示的值可以溢出其默認類型

比如,下例中的三個類型不確定常量均溢出了它們各自的默認類型,但是此程序編譯和運行都沒問題。

package main

// 三個類型不確定常量。
const n = 1 << 64          // 默認類型為int
const r = 'a' + 0x7FFFFFFF // 默認類型為rune
const x = 2e+308           // 默認類型為float64

func main() {
	_ = n >> 2
	_ = r - 0x7FFFFFFF
	_ = x / 2
}

但是下面這個程序編譯不通過,因為三個聲明的常量為類型確定常量。

package main

// 三個類型確定常量。
const n int = 1 << 64           // error: 溢出int
const r rune = 'a' + 0x7FFFFFFF // error: 溢出rune
const x float64 = 2e+308        // error: 溢出float64

func main() {}

每個常量標識符將在編譯的時候被其綁定的字面量所替代

常量聲明可以看作是增強型的C語言中的#define宏。 在編譯階段,所有的標識符將被它們各自綁定的字面量所替代。

如果一個運算中的所有運算數(shù)都為常量,則此運算的結果也為常量?;蛘哒f,此運算將在編譯階段就被估值。 下一篇文章將介紹Go中的常用運算符。

一個例子:

package main

const X = 3
const Y = X + X
var a = X

func main() {
	b := Y
	println(a, b, X, Y)
}

上面這段程序代碼將在編譯階段被重寫為下面這樣:

package main

var a = 3

func main() {
	b := 6
	println(a, b, 3, 6)
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號