Go語言核心之美 1.3-賦值及型別宣告篇
賦值(Assignment)
變數的值可以通過賦值操作符 = 來更新, v = 10。
x = 1 // 具名變數x
*p = true // 指標變數
person.name = "bob" // 結構體struct的欄位
count[x] = count[x] * scale // 陣列、切片或者map的某個元素
算數操作符和位操作符都有對應的一元操作符形式, v = v + x 等價於 v += x,例如:
count[x] *= scale
這樣的縮略形式能省去不少重複的工作,同時數字變數還能通過++遞增或者--遞減:v := 1 v++ // same as v = v + 1; v becomes 2 v-- // same as v = v - 1; v becomes 1 again
1.元組(tuple) 賦值元組賦值允許一次性給多個變數賦值。= 右邊的表示式會在左邊的變數被更新之前先求值,當=左右兩邊都有同樣的變數時,這種求值方式會非常有用,例如交換兩個變數的值:
x, y = y, x
a[i], a[j] = a[j], a[i]
再比如計算兩個數的最大公約數(GCD):func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
或者計算斐波那契數列第N個值:元組賦值也可以把一些零散的賦值組合起來:func fib(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }
i, j, k = 2, 3, 5
但是如果表示式較為複雜時,應該儘量避免元組賦值,分開賦值的可讀性會更好。
一些特定的表示式,例如函式呼叫有多個返回值,這種情況下 = 左邊必須有對應數量的變數來進行賦值:
f, err = os.Open("foo.txt") // 函式呼叫有兩個返回值
很多時候,函式會利用這種多返回值特性來額外返回一個值,這個值會說明函式的呼叫是否成功(可能是一個error型別變數err,或者bool型別變數ok)。在map中查詢某個值,型別斷言,從channel中接收值等等都會有兩個返回值,其中第二個值就是一個bool型別:
v, ok = m[key] // map lookup
v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive
Go語言還支援匿名變數 (用過函式式語言的讀者應該瞭解這種機制),我們可以把不想要的值賦給空白標示符(=左邊的變數數目和右邊的值數目必須相同):
_, err = io.Copy(dst, src) // 只關心Copy的成功與否,不關心具體Copy的位元組數,因此丟棄第一個值
_, ok = x.(T) // 只關心型別斷言的成功與否,不關心x的具體值,因此丟棄第一個值
2.可賦值性
上面的賦值語句是一種顯式賦值,但是某些情況下,會發生隱式賦值:在函式呼叫中,隱式賦值給函式引數;函式返回時,隱式賦值給return的運算元;還有類似下面的組合型別:
medals := []string{"gold", "silver", "bronze"}
這裡就是隱式賦值,等價的顯式形式是這樣的:medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
同樣的還有map、channel型別等,都支援這種隱式賦值。
無論是用顯式賦值或隱式賦值,只要 = 左右兩邊有相同的型別即可。
針對不同型別的具體賦值規則會在後續章節詳細講解。對於我們目前已經討論過的那些型別,規則是很簡單的:型別必須準確匹配(因此Go是強型別的靜態語言),nil可以被賦值給interface或者其它引用型別。 常量(constant)賦值在型別轉換時非常有靈活性,可以避免大多數顯式型別轉換:
const x = 112
var v float64 = x
fmt.Println(v) //output:112
兩個變數能否用 == 或 != 比較取決於可賦值性,a == b 只有在a = b可行時才能判斷。型別宣告
變數的型別定義了變數的一些個性化屬性,例如變數佔據的記憶體大小、在記憶體中的排列,變數內部的組織形式,變數支援的操作,變數的行為method等。
在實際專案中,很多自定義型別都有同樣的底層型別,例如:int可以是迴圈的索引,時間戳,檔案描述符或者一個月份;float64型別可以是車輛行駛速度,溫度。使用type就可以宣告具名型別,這樣就可以在底層型別之上構建自己的需要的時間戳,檔案描述符等型別。
type name underlying-type
具名型別宣告一般都發生在package級別,因此該型別是包內可見的,如果型別是匯出的(首字母大寫),那麼就是全域性可見的。為了解釋型別宣告,這裡把不同的溫度計量單位設定為不同的型別:package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
上面定義了兩個型別Celsius(攝氏度)和Fahrenheit(華氏度),作為溫度的兩種不同單位。即使兩個型別的底層型別是相同的float64,但是它們是完全不同的型別,所以它們彼此之間不能比較,也不能在算術表示式中結合使用,這種規則可以避免錯誤的使用兩種不同的溫度單位,因為兩個溫度單位的型別是不同的,CToF和FToC返回的兩個值也是完全不同的。對於每個型別T,都有一個對應的型別轉換T(x),可以把x轉換為T型別。當且僅當兩個型別具有相同的底層型別時,才能進行型別轉換,例如上文中的Celsius和Fahrenheit,或者兩個型別都是指標型別,指向的是同一個底層型別。
數值型別之間、string和[]byte之間都可以進行轉換,這些轉換可能會改變值的表現形式。例如,將浮點數轉化為整數會擷取掉小樹部分;將string轉化為[]byte切片會分配記憶體空間建立string的一份拷貝(記憶體拷貝往往是效能瓶頸之一)。總之型別轉換是編譯期完成的,在執行期是不會失敗的!
具名型別的底層型別決定了它的結構和表現形式,也決定了它支援的基本操作(可以理解為繼承自底層型別),就好像直接使用底層型別一樣。因此對於Celsius和Fahrenheit來說,float64支援的算術操作,它們都支援:
fmt.Printf("%g\n", BoilingC - FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF - CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF - FreezingC) // compile error: type mismatch
再比如:type temp int
func main() {
var x1 temp = 1
var x2 temp = 2
fmt.Println(x1 + x2)//output:3
}
如果兩個值有同樣的具名型別,那麼就可以用比較操作符==和<進行比較;或者兩個值,一個是具名型別,一個是具名型別的底層型別,也可以進行比較。但是兩個不同的具名型別是不可以直接比較的:
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
我們都知道浮點數是不精確的表達形式,因此兩個浮點數之間的比較是需要格外小心的。這裡注意最後一個型別轉換後浮點數之間的比較,Celsius(f)沒有改變f的值,是因為c和f兩個都是初始化為0,所以可以放心比較。
如果在專案中有些地方需要重複的去寫一個複雜型別時,那麼使用具名變數可以帶來極大的便利。
我們還可以為具名型別定義特有的行為,這些行為就是Go語言中的型別方法(method),在後續章節我們還會詳細講解。
下面的程式碼定義了Celsuis型別的一個methond:String,
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
很多自定義型別都會定義一個String method,因為在呼叫fmt包的函式去列印該型別時,String會控制該型別的列印方式(這裡其實是實現了Stringer這個介面,和其它OO語言不同,Go語言的介面實現是隱式的):
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
歡迎大家加入Go語言核心技術QQ群894864,裡面熱心大神很多哦!