天天看點

翻譯Go Blog: 常量

常量

Pob Pike

2014年8月24日

原文

介紹

Go是一門靜态語言,它不允許不同數字類型間的操作。你不能将一個浮點數(float64)和一個整數(int)相加,也不能将一個32位整數(int32)和一個通用整數(int)相加。這些寫法也是非法的:1e6*time.Second、math.Expr(1)、1<<('\t'+2.0)。在Go中,常量和變量不同,它很像是常數(regular number)。這篇文章将解釋其中緣由。

背景:C

在之前關于Go的思考中,我們讨論過C語言和C延伸出的語言允許你混合不同數字類型會引發很多問題。許多匪夷所思的bug,異常中斷和相容性問題都是由于表達式裡由不同長度的整型和符号類型(signedness)組成所導緻的。即便是對經驗豐富的C語言工程師,對于下面代碼的計算結果也會遲疑。

unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
           

最後的答案是多少?它是什麼類型的?有符号還是無符号的?

代碼裡還潛伏這bug。

C語言有一系列“通用算數轉換”規則,it is an indicator of their subtlety that they have changed over the years (introducing yet more bugs, retroactively).

當設計Go時,我們決定避免這個雷區,我們不允許不同數字類型之間的混合操作。如果你想将i和u相加,你必須顯示聲明你最終想要得到的類型。

var u uint
var i int
           

你可以寫 uint(i)+u 或者 i+int(u),這樣清楚地表達了相加操作的結果類型,你不能像C語言那樣寫 i+u。即使int是32bit位的,你也不能将int類型數字和int32類型數字混合操作。

這個強制要求消除類一些通常的bug和異常,他是Go中重要的屬性。但它也需要代價:有時需要程式員去用笨拙的數字類型裝換去裝飾代碼以清楚地表達出含義。

那常量怎麼辦?在上面的聲明裡,怎麼合法地寫 i=0 或者 u=0?0的類型是什麼?

var i int = int(0)
           

這種聲明方式顯然是不合理的。

我們很快意識到答案就是讓數字常量的工作方式和其他C類語言不同。在更多的思考和實驗之後,我們想出了我們相信最正确的設計,将程式員從總是要轉換常量中解放出來,他們可以這樣寫:math.Sqrt(2),編譯器也不會報錯。

總之,在Go中,常量總是行得通的。讓我們看看發生了什麼。

術語

首先在Go中,const是一個關鍵字,用一個标量(比如:2、3.14159、"scrumptious")來聲明一個名字。這樣的值(命名或其他形式)在Go中稱為常量。常量也可通過由常量建構的表達式建立,比如:2+3、2+3i、Pi/2、("go" + "pher")。

某些語言沒有常量,而另一些語言則具有常量的更一般定義或const單詞的應用。例如,在C和C++中,const是一個類型限定符,可以将更多複雜值的更複雜屬性進行編碼。

但是在Go中,常量僅僅是單一不可變的值,現在開始我們隻讨論Go中的常量。

字元串

這裡有很多種類型的數字常量:整型、浮點型、rune、有符号型、無符号型、虛數型、複數型。讓我們以簡單的字元串常量作為開始。字元串常量很容易了解,可以在其中探索Go中常量的類型問題。

一個字元串常量是雙引号閉合的文本(Go也支援原生字元串寫法,即用反引号閉合``````)。下面是一個字元串常量:

"Hello, 世界"
           

字元串常量的類型是什麼?string是錯誤的答案。

它是無類型字元串常量,即它沒有固定的類型。沒錯,它是一個字元串,但它的類型不是Go裡面的string。即便提供一個變量名,它仍然是無類型字元串常量:

const hello = "Hello, 世界"
           

這樣聲明之後,hello還是一個無類型字元串常量,一個無類型常量隻有一個沒有定義類型的值,是以它不受強制相同類型才可操作的限制。

正是無類型常量的概念讓我們可以在Go中自由使用常量。

那麼,有類型字元串常量是什麼?下面是一個有類型的例子:

const typedHello string = "Hello, 世界"
           

注意typedHello的聲明,有一個顯式string類型在等号前面。這意味着typedHell是string類型,它不可以被配置設定給其他類型。正确示例:

var s string
s = typeHello
fmt.Println(s)
           

錯誤示例:

type MyString string
var m Mystring
m = typedHello // Type error
fmt.Println(m)
           

變量m是Mystring類型的,它不能被其他類型值指派。它隻能被MyString類型值指派,像這樣:

const myStringHello MyString = "Hello, 世界"
m = myStringHello // OK
fmt.Println(m)
           

或者通過強制類型裝換來解決:

m = MyString(typedHello)
fmt.Println(m)
           

回到我們的無類型字元串常量,因為它沒有類型,所有把它指派給一個有類型的變量不會引起類型錯誤。我們可以這樣寫:

m = "Hello, 世界"
           

或者

m = hello
           

不像有類型常量typedHello和myStringHello,無類型常量"Hello, 世界"和hello沒有類型,所有把他們指派給任何類型相容string的變量都不會出錯。

這些無類型字元串常量當然是字元串,是以他們隻能用在string允許的地方,但他們沒有類型。

預設類型

做為一名Go程式員,你肯定見過這裡的聲明:

str := "Hello, 世界"
           

現在,你可能會問:“如果常量是無類型的,那比變量str是什麼類型的?”答案是string,無類型常量會有一個預設類型,會在需要時自動将自己轉換為該類型。對于無類型字元串常量,預設類型是string,是以

str := "Hello, 世界"
           
var str = "Hello, 世界"
           

意思都是

var str string = "Hello, 世界"
           

你可以把無類型常量當成一種在特殊空間的值,它們不會受到Go類型系統的大部分限制。但需要将它指派給有類型的變量時,它們把自己的預設類型告訴變量。在這個例子中,str變成了一個string類型的值,因為無類型字元串常量提供它們的預設類型string用來聲明。

在上面的聲明中,一個變量會被聲明并帶着類型和初始值。有時當我們使用常量時,并沒有明确的目标值,例如:

fmt.Printf("%s", "Hello, 世界")
           

輸出:

Hello, 世界
           

fmt.Printf的簽名是

func Printf(format string, a ...interface{}) (n int, err error)
           

format之後的參數是接口類型(interface)。但調用fmt.Printf時,使用一個無類型常量作為參數傳遞時,參數的具體類型是常量的預設類型。這個過程類似于我們之前使用無類型的字元串常量聲明初始化值時看到的過程。

你可以看到洗下面例子的結果,使用%v列印值,%T列印值的類型:

fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
fmt.Printf("%T: %v\n", hello, hello)
           
string: Hello, 世界
string: Hello, 世界
           

如果常量有類型,那麼會傳遞到接口,像下面這樣:

fmt.Printf("%T: %v\n", myStringHello, myStringHello)
           
main.MyString: Hello, 世界
           

總結一下,一個有類型常量會遵循Go裡所有的類型規則;一個無類型常量可以不用遵守,并且可以更自由地混合和比對。無類型常量的預設類型隻會在必要時且沒有其他類型資訊時被使用。

文法決定預設類型

一個無類型常量的預設類型被它的文法所決定。對于字元串常量,預設類型隻會是string。對于數字常量,預設類型有多種。整型常量預設是int,浮點型常量預設是float64,rune類型常量預設是rune(int32),虛數類型常量預設是comlex128。例子:

fmt.Printf("%T %v\n", 0, 0)
fmt.Printf("%T %v\n", 0.0, 0.0)
fmt.Printf("%T %v\n", 'x', 'x')
fmt.Printf("%T %v\n", 0i, 0i)
           
int 0
float64 0
int32 120
complex128 (0+0i)
           

Booleans

無類型boolean常量和無類型字元串常量是一樣的。true和false是無類型boolean常量,他們可以被指派給任何boolean變量,一旦确定了類型,就不能和boolean變量混合操作了:

type MyBool bool
const True = true
const TypedTrue bool = true
var m Mybool
mb = true       // OK
mb = True       // OK
mb = TypedTrue  // Bad
fmt.Println(mb)
           

浮點類型

浮點常量和boolean常量有很多相同的地方:

type MyFloat64 float64
const Zero = 0.0    // 無類型浮點常量
const TypedZero float64 = 0.0
var mf MyFloat64
mf = 0.0            // OK
mf = Zero           // OK
mf = TypedZero      // Bad
fmt.Println(mf)
           

唯一不同的地方是Go裡面有兩種浮點類型:float32和float64。浮點常量預設的類型是float64,但是可以把無類型浮點常量指派給float32類型的變量:

var f32 float32
f32 = 0.0
f32 = Zero      // OK: Zero is untyped
f32 = TypedZero // Bad: TypedZero is float64 not float32.
fmt.Println(f32)
           

我們來用浮點數介紹一下溢出的問題。

數字常量存儲在一個任意精度的數字空間了,它們是正常的數字。但當他們被配置設定給一個變量時,大小必須在變量的類型所支援的範圍内。我們可以給一個常量聲明一個非常大的值:

const Huge = 1e1000
           

雖然我們聲明了Huge這個常量,但是我們不能把Huge配置設定給其他變量,甚至無法列印出來。下面語句甚至不會編譯:

fmt.Println(Huge)
           

會報錯:"constant 1.00000e+1000 overflows float64"。但是Huge是可用的:我們可以在表達式裡用它和其他常量進行運算,如果結果在存在float64的範圍裡,如下:

fmt.Println(Huge / 1e999)
           
10
           

另外,浮點常量會有更高的精讀,是以用他們計算更加準确。在math包中定義的常量比float64類型的變量擁有更多的精度位。下面是math.Pi的定義:

Pi = 3.14159265358979323846264338327950288419716939937510582097494459
           

當Pi被配置設定給一個變量時,會有精讀丢失;該變量會是float64或float32類型來盡可能接近原來的值。比如:

pi := math.Pi
fmt.Println(pi)
           
3.141592653589793
           

數字常量擁有更多的精度位,意味着在進入比如:Pi/2這樣的計算中結果的精度很高,直到結果被配置設定給變量時精度才會丢失。并且在計算過程不會出現無窮大、溢出和NaN的問題。(如果表達式中有除以常量0的操作,在編譯時會報錯。

複數

複數常量也和浮點數常量類似。

type MyComplex128 complex128
const I = (0.0 + 1.0i)
const TypedI complex128 = (0.0 + 1.0i)
var mc MyComplex128
mc = (0.0 + 1.0)    // OK
mc = I              // Ok
mc = TypedI         // Bad
fmt.Println(mc)
           

複數常量的預設類型是complex128, 是由兩個float64的值組成,有很大的精度。

聲明一下,在例子中,我們寫了完整的表達式(0.0+1.0i),其實我們可以簡寫成0.0+1.0i、1.0i或者1i。

我們知道在Go中,數字常量隻是一個數字,如果一個複數沒有虛數部分,它會是什麼?實數嗎?

const Two = 2.0 + 0i
           

Two數一個無類型複數常量,即使它沒有虛數部分,表達式的文法定義了它的預設類型是complex128。是以,如果我們用它去聲明一個變量,預設類型将會是complex128。

s := Two
fmt.Printf("%T: %v\n", s, s)
           
complex128: (2+0i)
           

在數值上,Two既可以被存儲在float32空間裡,也可以存儲在float64空間裡,不會發生精度丢失。我們在初始化或配置設定時把Two配置設定給float64是沒問題的:

var f float64
var g float64 = Two
f = Two
fmt.Printf(f, "and", g)
           
2 and 2
           

即使Two是複數常量,也可以将其配置設定給浮點類型變量。這種跨界操作是很有用的。

整型

最後我們來到了整型。他們的種類更多,不同的大小、有符号或無符号等等,他們遵循一樣的規則:

type MyInt int
const Three = 3
const TypedThree int = 3
var mi Myint
mi = 3          // OK
mi = Three      // OK
mi = TypedThree // Bad
fmt.Println(mi)
           

上面的類型對所有的整數類型都适用,整數類型有:

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
           

(還有uint8也叫byte, int32也叫rune)。整數類型有很多,但是在整數常量的工作方式都是相似的,現在我們來具體看一看。

如上所述,整型有兩種形式,每種形式都有自己的預設類型:對于像123、0xFF、-14這些簡單的常量,預設類型是int;像'a'、'世'、'\r'這些單引号單字元,預設類型是rune。

沒有哪種常量形式的預設類型無符号整型。但是我們可以利用無類型常量的靈活性,初始化出無符号整型變量。這就和我們可以用虛部為0的複數初始化出float64變量一樣。下面有幾中初始化uint的方式,每種方法都是明确指定了類型為uint:

var u uint = 17
var u = uint(17)
u := uint(17)
           

整型也有類型浮點數那樣數值範圍的問題,不同整型之間的轉換、配置設定可能會出現問題。可能會有兩個問題發生:大範圍的值轉換為小範圍的值;負數配置設定到無符号整型上。例如:int8的範圍是-128到127,是以如果常量超出這個範圍将不能被配置設定到一個int8類型的變量上:

var i8 int8 = 128 // Error: too large.
           

同樣,uint8(或者叫byte)的範圍是0到255,不在這個範圍中的常量也不能被配置設定到一個uint8變量上:

var u8 uint8 = -1 // Error: negative value.
           

下面的代碼也會被類型檢查捕捉到錯誤:

type Char byte
var c Char = '世' // Error: '世' has value 0x4e16, too large
           

如果編譯器檢查出常量的用法錯誤,真實太悲傷了。

練習:最大的無符号整型

這裡有一個有意思的小練習。我們如何用常量表示uint中的最大值呢?如果我們是說uint32,我們可以這個樣寫:

const MaxUint32 = 1<<32 - 1
           

但是我們要的是uint,而不是uint32。int和uint類型沒有确定的bit位數,32位或者64位。因為具體的bit位數取決架構,我們不能寫一個簡單的值。

...省略...

最簡單的uint值是有類型常量uint(0),如果uint是32位,那麼uint(32)就用32位0bit,反之uint是64位,那麼uint(0)就有64位0bit。如果我們将這些bit位倒置成1,我們将會得到uint的最大值:

const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
           

無論目前環境uint32的bit位是多少,這個常量都正确表示uint變量能承載的最大值。

數字

在Go中無類型常量的概念表示所有的數字常量,無論整型、浮點型、複數型還是字元值,都存儲在一種統一的空間裡。我們将它們作用在變量、複制和運算中時,類型才會有意義。隻要我們在數字常量的世界裡,我們就可以随意混合不同的類型進行運算,下面所有的常量的數值都是1:

1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i
           

是以,即使他們有不同的隐含預設類型,寫做無類型的常量可以被配置設定任何整型變量:

var f float32 = 1
var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = '\x01'
var p uintptr = '\u0001'
var r complex64 = 'b' - 'a'
var b byte = 1.0 + 3i - 3.0i

fmt.Println(f, i, u, c, p, r, b)
           
1 1 1 1 1 (1+0i) 1
           

你甚至可以這樣寫:

var f = 'a' * 1.5
fmt.Println(f)
           
145.5
           

盡管在Go中同一個表達式中混合使用浮點數和整數變量或者int和int32變量是不合法的,但是這種靈活性讓下面的寫法可行,即不同類型的數字常量是可以混合的:

sqrt2 := math.Sqrt(2)
           
const millisecond = time.Second/1e3
           
bigBufferWithHeader := make([]byte, 512+1e6
           

結果也是你期望的那樣。

因為在Go裡,數字常量的工作方式就像普通的數字一樣。

go