1. 程式人生 > >Go語言核心之美 1.3-賦值及型別宣告篇

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,裡面熱心大神很多哦!