天天看點

Go語言基礎4 - 資料(基本資料結構)概述資料初始化

概述

我們将用幾節來學習Go語言基礎,本文結構如下:

資料
    new 配置設定
    構造函數與複合字面
    make 配置設定
    數組
    切片
    二維切片
    映射
    列印
    追加
 初始化
    常量
    變量
    init 函數           

複制

資料

本節包含了 Go 為變量配置設定記憶體的方式,和常用的數組,map兩種資料結構。

Go提供了兩種配置設定方式,即内建函數 new 和 make。

關鍵點:

  • make 隻适用于映射、切片和信道且不傳回指針。
  • 若要獲得明确的指針, 請使用 new 配置設定記憶體。

new 配置設定

new 函數格式為: new(T)

特點:它傳回一個指針, 該指針指向新配置設定的,類型為 T 的零值

内建函數 new 是個用來配置設定記憶體的内建函數, 但與其它語言中的同名函數不同,它不會初始化記憶體,隻會将記憶體置零。

Go 的 new比于java的情形是,java可以通過 new 執行構造來初始化一個對象,而Go不能初始化(賦初值),它隻能置為”零值“

也就是說,new(T) 會為類型為 T 的新項配置設定已置零的記憶體空間, 并傳回它的位址,也就是一個類型為 *T 的值。用Go的術語來說,

它傳回一個指針, 該指針指向新配置設定的,類型為 T 的零值

這樣的設計,使得無需像Java那樣面對不同對象的豐富多彩的構造函數和參數。

既然 new 傳回的記憶體已置零,就不必進一步初始化了,使用者隻需用 new 建立一個新的對象就能正常工作。

例如:

  • bytes.Buffer 的文檔中提到“零值的 Buffer 就是已準備就緒的緩沖區。"
  • sync.Mutex 并沒有顯式的構造函數或 Init 方法, 而是零值的 sync.Mutex 就已經被定義為已解鎖的互斥鎖了。

p := new(SyncedBuffer) // type *SyncedBuffer

var v SyncedBuffer // type SyncedBuffer

如上的兩種方式,都會配置設定好記憶體空間,而類型是不同的。

構造函數與複合字面

有些場景下,仍然需要一個初始化構造函數,就像 os 包中的這段代碼所示:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}           

複制

上面的代碼過于冗長。我們可通過複合字面來簡化它:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}           

複制

注意 File{fd, name, nil, 0} 這樣的寫法就是

複合字面

的寫法。該表達式在每次求值時都會建立新的執行個體。

複合字面的字段必須

按順序全部列出

。但如果以

字段:值

對的形式明确地标出元素,初始化字段時就可以按任何順序出現,未給出的字段值将賦予零值。 是以,我們可以用如下形式:

return &File{fd: fd, name: name}           

複制

make 配置設定

内建函數 make 的格式為: make(T, args)

特點:它隻用于建立切片、映射和信道,并傳回類型為 T(而非 *T)的一個已初始化 (而非置零)的值。

切片、映射和信道 本質上為引用資料類型,在使用前必須初始化。 例如,切片是一個具有三項内容的描述符,包含一個指向(數組内部)資料的指針、長度以及容量, 在這三項被初始化之前,該切片為 nil。

對于切片、映射和信道,make 用于初始化其内部的資料結構并準備好将要使用的值。

例如:

make([]int, 10, 100) 配置設定一個具有100個 int 的數組空間,接着建立一個長度為10, 容量為100并指向該數組中前10個元素的切片結構

new([]int) 會傳回一個指向新配置設定的,已置零的切片結構, 即一個指向 nil 切片值的指針。

下面的例子闡明了 new 和 make 之間的差別:

var p *[]int = new([]int)       // 配置設定切片結構;*p == nil;基本沒用
var v  []int = make([]int, 100) // 切片 v 現在引用了一個具有 100 個 int 元素的新數組

// 沒必要的複雜:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// 習慣用法:
v := make([]int, 100)           

複制

再次說明關鍵點:

  • make 隻适用于映射、切片和信道且不傳回指針。
  • 若要獲得明确的指針, 請使用 new 配置設定記憶體。

數組

在規劃記憶體布局時,數組是非常有用的,有時還能避免過多的記憶體配置設定, 在Go中,數組主要用作切片的構件,在建構切片時使用。

數組在Go和C中的主要差別。在Go中:

  • 數組是值。将一個數組賦予另一個數組會複制其所有元素。
  • 若将某個數組傳入某個函數,它将接收到該數組的一份副本而非指針。
  • 數組的大小是其類型的一部分。類型 [10]int 和 [20]int 是不同的。

數組為值的屬性很有用,但代價高昂;若你想要C那樣的行為和效率,你可以傳遞一個指向該數組的指針。

在 Go 中,更習慣的的用法是使用 切片。

切片

切片通過對數組進行封裝,為有序列的資料提供了更通用、強大而友善的方式。

除了矩陣變換這類需要明确次元的情況外,Go中的大部分數組程式設計都是通過切片來完成的。

切片儲存了對底層數組的引用,若你将某個切片賦予另一個切片,它們會引用同一個數組。 若某個函數将一個切片作為參數傳入,則它對該切片元素的修改對調用者而言同樣可見, 這可以了解為傳遞了底層數組的指針。

修改長度:隻要切片不超出底層數組的限制,它的長度就是可變的,隻需産生新的切片再次指向自身變量即可。

切片的長度:

len(切片)           

複制

切片的容量可通過内建函數 cap 獲得,它将給出該切片可取得的最大長度。函數為:

cap(切片)           

複制

若資料超出其容量,則會重新配置設定該切片。傳回值即為所得的切片。

向切片追加東西的很常用,是以有專門的内建函數 append。

一般情況下,如果我們要寫一個 append 方法的話,最終傳回值必須傳回切片。示例:

func Append(slice, data[]byte) []byte {
        l := len(slice)
        if l + len(data) > cap(slice) {  // 重新配置設定
            // 為了後面的增長,需配置設定兩份。
            newSlice := make([]byte, (l+len(data))*2)
            // copy 函數是預聲明的,且可用于任何切片類型。
            copy(newSlice, slice)
            slice = newSlice
        }
        slice = slice[0:l+len(data)]
        for i, c := range data {
            slice[l+i] = c
        }
        return slice
    }           

複制

如上,輸入參數是切片和插入的元素值,傳回值是切片,注意切片的長度會發生變化。

因為盡管 Append 可修改 切片 的元素,但切片自身(其運作時資料結構包含指針、長度和容量)是通過值傳遞的。

二維切片

要建立等價的二維數組或切片,就必須定義一個數組的數組, 或切片的切片,示例:

type Transform [3][3]float64  // 一個 3x3 的數組,其實是包含多個數組的一個數組。
type LinesOfText [][]byte     // 包含多個位元組切片的一個切片。           

複制

每行都有其自己的長度:

由于切片長度是可變的,是以其内部可能擁有多個不同長度的切片。

映射 (map)

映射 是Go中 資料結構中的 map結構實作,即 key: value的形式存儲。

映射的值可以是各種類型。

映射的鍵可以是整數、浮點數、複數、字元串、指針、接口等。

映射的鍵(或者叫索引)可以是任何相等性操作符支援的類型, 如整數、浮點數、複數、字元串、指針、接口(隻要其動态類型支援相等性判斷)、結構以及數組。 切片不能用作映射鍵,因為它們的相等性還未定義。與切片一樣,映射也是引用類型。

如果将映射作為參數傳入函數中,并更改了該映射的内容,則此修改對調用者同樣可見。

映射可使用一般的複合字面文法進行建構,其鍵-值對使用逗号分隔,有點像JSON:

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}           

複制

擷取值:

offset := timeZone["EST"]           

複制

注意:若試圖通過映射中不存在的鍵來取值,就會傳回與該映射中項的類型對應的零值。例如,若某個映射包含整數,當查找一個不存在的鍵時會傳回 0。

判斷某個值是否存在:

seconds, ok = timeZone[tz]           

複制

上面是慣用的 "逗号 ok” 法:

  • 若 tz 存在, seconds 就會被賦予适當的值,且 ok 會被置為 true; - 若不存在,seconds 則會被置為零,而 ok 會被置為 false。

若僅需判斷映射中是否存在某項而不關心實際的值,可使用空白辨別符

_

來代替該值的一般變量。

_, present := timeZone[tz]           

複制

要删除映射中的某項,可使用内建函數

delete

。即便對應的鍵不在該映射中,此操作也是安全的。

delete(timeZone, "PDT")             

複制

列印

Go的格式化列印風格和C的 printf 類似,但卻更加豐富而通用。 這些函數位于 fmt 包中,且函數名首字母均為大寫:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。

看例子:

// 以f 結尾的這幾個,傳入格式化字元串作為參數, 不換行
fmt.Printf("hello, %v \n","zhang3")
fmt.Fprintf(os.Stdout,"hello, %v \n","zhang3")
str := fmt.Sprintf("hello, %v \n","zhang3")

//下面這幾個,會換行
fmt.Println(str)
// 注意下面這個,會自動在元素間插入空格
fmt.Fprintln(os.Stdout,"f1","f2","f3")           

複制

Sprintf 用于構造字元串: 字元串函數(Sprintf 等)會傳回一個字元串,而不是寫入到資料流中。

Fprint 用于寫入到各種流中:fmt.Fprint 一類的格式化列印函數可接受任何實作了 io.Writer 接口的對象作為第一個實參;比如 os.Stdout 與 os.Stderr 。

下面對 Printf 支援的格式化的字元做一些說明:

-- 格式: %d

像 %d 不接受表示符号或大小的标記, 會根據實際的類型來決定這些屬性。

var x uint64 = 1<<64 - 1 // x 是無符号整數, 下面的 int64(x) 轉換為有符合整數
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))           

複制

将列印

18446744073709551615 ffffffffffffffff; -1 -1           

複制

-- 格式: %v

%v 可了解為 實際的 value。

它還能列印任意值,甚至包括數組、結構體和映射。

fmt.Printf("%v\n", timeZone)  // 或隻用 fmt.Println(timeZone)           

複制

這會輸出

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]           

複制

%+v 和 %#v

當列印結構體時,格式 %+v 會帶上每個字段的字段名,而格式 %#v 會帶上類型。

type T struct {
    a int
    b float64
    c string
  }
  t := &T{ 7, -2.35, "abc\tdef" }
  fmt.Printf("%v\n", t)
  fmt.Printf("%+v\n", t)
  fmt.Printf("%#v\n", t)           

複制

将列印

&{7 -2.35 abc   def} // 請注意其中的&符号
&{a:7 b:-2.35 c:abc     def} // 有了字段名
&main.T{a:7, b:-2.35, c:"abc\tdef"} //有了類型           

複制

-- 格式:%q

當遇到 string 或 []byte 值時, 可使用 %q 産生帶引号的字元串;而格式 %#q 會盡可能使用反引号。

--格式:%x

%x 還可用于字元串、位元組數組以及整數,并生成一個很長的十六進制字元串, 而帶空格的格式(% x)還會在位元組之間插入空格。

--格式: %T

它會列印某個值的類型.

fmt.Printf("%T\n", timeZone)           

複制

會列印

map[string] int           

複制

-- 為結構圖自定義輸出

類似 java 中的 toString(),對結構圖自定義類型的預設格式,隻需為該類型定義一個具有 String() string 簽名的方法。對于我們簡單的類型 T,可進行如下操作。

func (t *T) String() string {
        return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}

fmt.Printf("%v\n", t)           

複制

會列印出如下格式:

7/-2.35/"abc\tdef"           

複制

-- 任意數量的

Printf 的簽名為其最後的實參使用了 ...interface{} 類型,這樣格式的後面就能出現任意數量,任意類型的形參了。

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

複制

在 Printf 函數的實作中,v 看起來更像是 []interface{} 類型的變量,但如果将它傳遞到另一個變參函數中,它就像是正常實參清單了。實際上,它直接将其實參傳遞給 fmt.Sprintln 進行實際的格式化。

// Println 通過 fmt.Println 的方式将日志列印到标準記錄器。
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output 接受形參 (int, string)
}           

複制

注意上面的 ...interface{} 和 v... 的寫法。

追加 ( append 函數 說明 )

append 函數的簽名就像這樣:

func append(slice []T, 元素 ...T) []T           

複制

其中的 T 為任意給定類型的占位符。實際上,你無法編寫一個類型 T 由調用者決定的函數。這也就是為何 append 為内建函數的原因:它需要編譯器的支援。

append 會在切片末尾追加元素并傳回結果。我們必須傳回結果, 原因是,底層數組可能會被改變(注意數組的長度是類型的一部分)。

以下簡單的例子

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)           

複制

将列印

[1 2 3 4 5 6]           

複制

将一個切片追加到另一個切片很簡單:在調用的地方使用 ...

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)           

複制

如果沒有 ...,它就會由于類型錯誤而無法編譯,因為 y 不是 int 類型的。三個點符号 “

...

” 的作用有點像“ 展開 ” 的作用,即将 y這個切片的元素放到了這裡。

初始化

GO 的huaGo的初始化很強大,在初始化過程中,不僅可以建構複雜的結構,還能正确處理不同包對象間的初始化順序。

常量

常量在編譯時被建立,即便函數中定義的局部變量也一樣。

常量隻能是數字、字元(符文)、字元串或布爾值。

由于編譯時的限制, 定義它們的表達式必須是可被編譯器求值的常量表達式。例如 1<<3 就是一個常量表達式。

枚舉常量

枚舉常量使用枚舉器 iota 建立。由于 iota 可為表達式的一部分,而表達式可以被隐式地重複,這樣也就更容易建構複雜的值的集合了。

type ByteSize float64

  const (
      // 通過賦予空白辨別符來忽略第一個值
      _           = iota // ignore first value by assigning to blank identifier
      KB ByteSize = 1 << (10 * iota)
      MB
      GB
      TB
      PB
      EB
      ZB
      YB
  )           

複制

變量

變量的初始化與常量類似,但其初始值也可以是在運作時才被計算的一般表達式。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)           

複制

init 函數

每個源檔案都可以通過定義自己的無參數 init 函數來設定一些必要的狀态。格式為:

func init() { 
    ...
 }           

複制

而 init 方法執行結束,就意味着初始化結束了:隻有該包中的所有變量聲明都通過它們的初始化器求值後 init 才會被調用, 而那些 init 隻有在所有已導入的包都被初始化後才會被求值。

init 函數還常被用在程式真正開始執行前,檢驗或校正程式的狀态。示例:

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath 可通過指令行中的 --gopath 标記覆寫掉。
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
  }           

複制