Go36-4,5,6-變量

上篇

Go語言中的程序實體包括變量、常量、函數、結構體和接口。Go語言是靜態類型的編程語言,所以我們在聲明變量或常量的時候都需要指定它們類型,或者給予足夠的信息以使Go語言能夠推導出它們的類型。

聲明變量

聲明變量的方式:

var name string
var name = "Adam"
name := "Bob"  // 短變量聲明

第三種短變量聲明只能在函數體內部使用。

知識點

類型推斷
後兩種在聲明的同時還進行了賦值,沒有顯示的指定類型,而是利用了Go的類型推斷。

代碼重構
我們通常把“不改變某個程序與外界的任何交互方式和規則,而只改變其內部實現”的代碼修改方式,叫做對該程序的重構。

代碼塊
在Go語言中,代碼塊一般就是一個由花括號括起來的區域,里面可以包含表達式和語句。
Go語言本身以及我們編寫的代碼共同形成了一個非常大的代碼塊,也叫全域代碼塊。

空代碼塊

func main() {}

變量重聲明

變量重聲明,對已經聲明過的變量再次聲明:

  1. 由於變量的類型在其初始化時就已經確定了,所以對它再次聲明時賦予的類型必須與其原本的類型相同,否則會產生編譯錯誤。
  2. 變量的重聲明只能發生在某一個代碼塊中。如果與當前的變量重名的是外層代碼塊中的變量,那麼就是另外一種含義了。
  3. 變量的重聲明只有在使用短變量聲明時纔會發生,否則也無法通過編譯。如果要聲明新變量,就使用var關鍵字聲明,用新的變量名。
  4. 被“聲明並賦值”的變量必須有多個,並且其中至少有一個是新的變量。這時纔可以說對其中的舊變量進行了重聲明。

重聲明只在短變量聲明中出現,並且是多個變量的聲明中出現。給新的變量賦值,給舊的變量賦新值。
變量重聲明,允許我們再使用短變量聲明時不用理會被賦值的多個變量中是否有包含舊變量。好處是寫代碼時的便利。

package main

import (
    "os"
    "io"
    "fmt"
)

func main() {
    n1, err := io.WriteString(os.Stdout, "Test1")
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("寫入字節(byte)數:", n1)

    n2, err := io.WriteString(os.Stdout, "測試2")  // 對err進行了重聲明
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("寫入字節(byte)數:", n2)

    n2, err = io.WriteString(os.Stdout, "測試三")  // 這裏都是舊變量,沒有新變量,所以用的是賦值=
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("寫入字節(byte)數:", n2)
}

小結

使用關鍵字var和短變量聲明,都可以實現對變量的“聲明並賦值”。
前者可以被用在任何地方,而後者只能被用在函數或者其他更小的代碼塊中。
前者無法對已有的變量進行聲明,就是無法處理新舊變量混在一起的情況。可以使用後者的變量重聲明實現。
共同點是,都是基於“類型推斷”。

中篇

一個程序實體的作用域總是會被限制在某個代碼塊中。而這個作用域最大的用處,就是對程序實體的訪問權限的控制。對“高內聚,低耦合”這種程序設計思想的實踐恰恰可以從這里開始。

變量作用域

變量重名的示例:

package main

import "fmt"

var block = "package"

func main() {
    block := "function"
    {
        block := "inner"
        fmt.Printf("block here is %s\n", block)
    }
    fmt.Printf("block here is %s\n", block)
}

/* 執行結果
PS H:\Go\src\Go36\article05\example01> go run main.go
block here is inner
block here is function
PS H:\Go\src\Go36\article05\example01>
*/

上面的代碼中有4個代碼塊:

  • 全域代碼塊
  • main包代表的代碼塊,var block = "package"
  • main函數代表的代碼塊,block := "function"
  • main函數中用大括號包起來的代碼塊,block := "inner"

在後3個代碼塊中都聲明瞭一個block的變量,賦值爲不同的字符串。
聲明重名的變量是無法編譯通過的,但是這是對同一代碼塊內部而言的。上面的例子中是在不同的代碼塊中進行的聲明。

引用變量時的查找過程
首先,會在當前代碼塊中查找變量。不包含任何的子代碼塊。
其次,如果當前代碼塊沒有什麼此變量名,一層一層往上層的代碼塊查找。
最後,如果都找不到,則編譯器會報錯。

下篇

不同代碼塊的變量可以重名,並且類型也可以不同。必要時,在使用之前,要先對變量的類型進行檢查。

示例

下面代碼中的container變量,雖然類型不同,但是都可以使用下標[0]、[1]、[2],獲取到值:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    fmt.Println(container[0], container[1], container[2])
}

如果,要判斷變量的類型,就要使用“類型斷言”表達式。

類型斷言

語法:x.(T)。
其中的x代表要被判斷類型的那個值。T是要判斷的類型。針對上面示例中的類型斷言:

value, ok := interface{}(container).([]string)

上面是一條賦值語句,賦值符號的右邊,就是一個類型斷言表達式。
先把變量container的值轉換爲空接口的值interface{}(container)。然後再判斷他的類型是否爲後面.()中的類型。
有2個返回值,value和ok。ok是布爾類型,代碼類型判斷的結果:

  • 如果是true,被判斷的值自動轉換爲.()中的類型的值,並且賦值給value。
  • 如果是false,value會賦值爲nil,就是空。

不接收ok
這裏ok也是可以沒有的:

value := interface{}(container).([]string)

這樣的話,如果類型不對,就是引發異常panic。

轉爲空接口的語法
在Go語言中,interface{}代表空接口。任何類型的值都可以很方便地被轉換成空接口的值,語法:interface{}(x)。
一對不包裹任何東西的花括號,除了可以代表空的代碼塊之外,還可以用於表示不包含任何內容的數據結構(或者說數據類型)。

字面量
小括號中[]string是一個類型字面量。所謂類型字面量,就是用來表示數據類型本身的若干個字符。
比如:string是表示字符串類型的字面量,uint8是表示8位無符號整數類型的字面量。

優化示例代碼

修改開始的示例,在打印前,先對變量的類型進行判斷,只有map或切片類型才進行打印:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    // 打印之前先要做判斷,只有map或者切片類型才能通過
    _, ok1 := interface{}(container).([]string)
    _, ok2 := interface{}(container).(map[int]string)
    if !(ok1 || ok2) {
        fmt.Printf("ERROR: 類型斷言失敗 %T\n", container)
        return
    }
    fmt.Println(container[0], container[1], container[2])
}

另外還有一種switch語句的實現形式:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    switch v := interface{}(container).(type) {
    case []string:
        fmt.Println("[]string:", v)
    case map[int]string:
        fmt.Println("map[int]string:", v)
    default:
        fmt.Printf("ERROR: 類型斷言失敗 %T\n", container)
        return
    }
}

類型轉換的坑

類型轉換表達式的語法:T(x)。
其中的x可以是一個變量,也可以是一個代表值的字面量(比如1.23和struct{}),還可以是一個表達式。如果是表達式,表達式的結果只能是一個值。
x被叫做源值,它的類型就是源類型。T代表的類型是目標類型。

數值類型間互轉

對於整數類型值、整數常量之間的類型轉換,原則上只要源值在目標類型的可表示範圍內就是合法的。
上面說的只是語法上合法,但是轉換後的結果可能是可坑。比如,如果源整數類型的可表示範圍大,而目標類型的可表示範圍小:

package main

import "fmt"

func main() {
    var srcInt = int16(-255)  // 1111111100000001
    dstInt := int8(srcInt)  // 00000001,簡單粗暴的截掉最前面的8位
    fmt.Println(srcInt, dstInt)
}
/* 執行結果
PS H:\Go\src\Go36\article06\example04> go run main.go
-255 1
PS H:\Go\src\Go36\article06\example04>
*/

在計算機系統中,數值一律用補碼來表示和存儲。原因在於,使用補碼,可以將符號位和數值域統一處理;同時,加法和減法也可以統一處理。補碼就是原碼的各位求反再加1。比如-255:
原碼: 1000 0000 1111 1111
反碼: 1111 1111 0000 0000 最高位是符號位,不反轉。
補碼: 1111 1111 0000 0001
類型轉換的很簡單粗暴,直接把最高的8位截掉,並不處理符號位,結果就是0000 0001,所以轉換後的值就變成1了。

浮點類型轉換
如果把浮點數轉換爲整數,則小數部分會被全部截掉:

package main

import "fmt"

func main() {
    var x = float64(1.9999999)
    y := int(x)
    fmt.Println(x, y)
}
/* 執行結果
PS H:\Go\src\Go36\article06\example05> go run main.go
1.9999999 1
PS H:\Go\src\Go36\article06\example05>
*/

整數轉字符串

直接把一個整數值轉換爲一個string類型的值是可行的。但是,被轉換的整數值應該是一個有效的Unicode碼點,否則轉換的結果將會是"�"。字符'�'的Unicode碼點是U+FFFD。它是Unicode標準中定義的Replacement Character,專用於替換那些未知的、不被認可的以及無法展示的字符。無效的碼點有很多,如果自己要搞一個測試,那麼就用-1吧:

package main

import "fmt"

func main() {
    fmt.Println(string(-1))  // 一個無效的Unicode碼點
    fmt.Println(string(65))  // 字符A
    fmt.Println(string(24464))  // 中文
}

字符串與切片

一個值在從string類型轉爲[]byte類型時,其中UTF-8編碼的字符串會被拆分成零散、獨立的字節。這樣只有ASCII碼的那部分字符是一個字節代碼一個字符的。而其他字符,比如中文(UTF-8裏中文字符用3個字節表示),會被拆開成3個字節。而且由於UTF-8的長度是可變的,這樣還要想辦法判斷那幾個字節應該是一個字符。
可以轉爲[]rune類型,這樣轉換時,每個字符會被拆開成一個個的Unicode字符。

package main

import "fmt"

func main() {
    s := "你好"
    s1 := []byte(s)
    fmt.Println(s1)
    s2 := []rune(s)
    fmt.Println(s2)
    for _, v := range(s1) {
        fmt.Print(string(v))  // 亂碼
    }
    fmt.Println()
    for _, v := range(s2) {
        fmt.Print(string(v))
    }
    fmt.Println()
}
/* 執行結果
PS H:\Go\src\Go36\article06\example07> go run main.go
[228 189 160 229 165 189]
[20320 22909]
ä½ å¥½
你好
PS H:\Go\src\Go36\article06\example07>
*/

別名類型 和 潛在類型

別名類型聲明與類型再定義之間的區別,以及由此帶來的它們的值在類型轉換、判等、比較和賦值操作方面的不同。

別名類型

可以用關鍵字type聲明自定義的各種類型。比如,可以聲明別名類型:

type MyString = string

上面的聲明語句表示,MyString是string類型的別名類型。別名類型與其源類型除了在名稱上以外,都是完全相同的。別名類型主要是爲了代碼重構而存在的。
Go語言的基本類型中就存在兩個別名類型。byte是uint8的別名類型,而rune是int32的別名類型。

潛在類型

另外一種聲明:

type MyString2 string  // 注意,這裏沒有等號

這種方式也可以被叫做對類型的再定義。這裏MyString2是一個新的類型,和string是不同的類型。string可以被稱爲MyString2的潛在類型。
潛在類型相同的不同類型的值之間是可以進行類型轉換的。因此,MyString2類型的值與string類型的值可以使用類型轉換表達式進行互轉。
但是,[]MyStrings 和 []string 是不同的潛在類型,不能做類型轉換。
另外,即使是相同的潛在類型,也不能進行判等或比較,變量之間不能賦值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章