天天看點

Go語言與資料庫開發:01-02. 關于命名:. 聲明:.變量. 簡短變量. 指針. New函數. 變量的生命周期及GC. 指派. 類型. 包和檔案. 作用域

接下來,開始了解go語言的程式結構,基礎要打牢。

go語言和其他程式設計語言一樣,一個大的程式是由很多小的基礎構件組成的。變量儲存值,簡

單的加法和減法運算被組合成較複雜的表達式。基礎類型被聚合為數組或結構體等更複雜的

資料結構。然後使用if和for之類的控制語句來組織和控制表達式的執行流程。然後多個語句被

組織到一個個函數中,以便代碼的隔離和複用。函數以源檔案和包的方式被組織。

在go中是區分大小寫的;關鍵字不能用于自定義名字;

go語言的風格是盡量使用短小的名字,對于局部變量尤其是這樣;你會經常看到i之類的短名字,

而不是冗長的theloopindex命名。通常來說,如果一個名字的作用域比較大,生命周期也比較長,

那麼用長的名字将會更有意義。

在習慣上,go語言程式員推薦使用 駝峰式 命名,當名字有幾個單詞組成的時優先使用大小寫

分隔,而不是優先用下劃線分隔。是以,在标準庫有quoterunetoascii和parserequestline

這樣的函數命名,但是一般不會用quote_rune_to_ascii和parse_request_line這樣的命名。

聲明語句定義了程式的各種實體對象以及部分或全部的屬性。

go語言主要有四種類型的聲明語句:

var 變量

const 常量

type 類型

func 函數實體對象的聲明

一個go語言編寫的程式對應一個或多個以.go為檔案字尾名的源檔案中。

每個源檔案以包的聲明語句開始,說明該源檔案是屬于哪個包。

一個聲明的例子:

// boiling prints the boiling point of water.

package main

import "fmt"

const boilingf = 212.0

func main() {

var f = boilingf

var c = (f - 32) * 5 / 9

fmt.printf("boiling point = %g°f or %g°cn", f, c)

// output:

// boiling point = 212°f or 100°c

}

其中常量boilingf是在包一級範圍聲明語句聲明的,然後f和c兩個變量是在main函數内部聲明

的聲明語句聲明的。在包一級聲明語句聲明的名字可在整個包對應的每個源檔案中通路,而

不是僅僅在其聲明語句所在的源檔案中通路。相比之下,局部聲明的名字就隻能在函數内部

很小的範圍被通路。

var 變量名字 類型 = 表達式

其中“類型”或“= 表達式”兩個部分可以省略其中的一個。

如果省略的是類型資訊,那麼将根據初始化表達式來推導變量的類型資訊。

如果初始化表達式被省略,那麼将用零值初始化該變量。

不同類型變量對應的零值是不同的:

數值類型變量對應的零值是0

布爾類型變量對應的零值是false

字元串類型對應的零值是空字元串

接口或引用類型(包括slice、map、chan和函數)變量對應的零值是nil

數組或結構體等聚合類型對應的零值是每個元素或字段都是對應該類型的零值

零值初始化機制可以確定每個聲明的變量總是有一個良好定義的值,是以在go語言中不存在未初始化的變量。

可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明并初始化一組變量。

如果省略每個變量的類型,将可以聲明多個類型不同的變量(類型由初始化表達式推導):

var i, j, k int // int, int, int

var b, f, s = true, 2.3, "four" // bool, float64, string

初始化表達式可以是字面量或任意的表達式。

在包級别聲明的變量會在main入口函數執行前完成初始化,

局部變量将在聲明語句被執行到的時候完成初始化。

一組變量也可以通過調用一個函數,由函數傳回的多個傳回值初始化:

var f, err = os.open(name) // os.open returns a file and an error

在函數内部,有一種稱為簡短變量聲明語句的形式可用于聲明和初始化局部變量。

以“名字:= 表達式”形式聲明變量,變量的類型根據表達式來自動推導。

例如:

anim := gif.gif{loopcount: nframes}

freq := rand.float64() * 3.0

t := 0.0

因為簡潔和靈活的特點,簡短變量聲明被廣泛用于大部分的局部變量的聲明和初始化

簡短變量聲明語句也可以用來聲明和初始化一組變量:

i, j := 0, 1

但是這種同時聲明多個變量的方式應該限制隻在可以提高代碼可讀性的地方使用,比如for語

句的循環的初始化語句部分。

請記住“:=”是一個變量聲明語句,而“=‘是一個變量指派操作。

簡短變量聲明左邊的變量可能并不是全部都是剛剛聲明的。如果有一些已經在相同的詞法域聲

明過了,那麼簡短變量聲明語句對這些已經聲明過的變量就隻有指派行為了。

也就是說,已經聲明過了以後,後續對簡短變量就隻能有指派行為了。

簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量才和指派操作語句等價,如果變量

是在外部詞法域聲明的,那麼簡短變量聲明語句将會在目前詞法域重新聲明一個新的變量。

一個變量對應一個儲存了變量對應類型值的記憶體空間。

一個指針的值是另一個變量的位址。一個指針對應變量在記憶體中的存儲位置。并不是每一個

值都會有一個記憶體位址,但是對于每一個變量必然有對應的記憶體位址。通過指針,我們可以

直接讀或更新對應變量的值,而不需要知道該變量的名字(如果變量有名字的話)。

如果用“var x int”聲明語句聲明一個x變量,那麼&x表達式(取x變量的記憶體位址)将産生一個

指向該整數變量的指針,指針對應的資料類型是 *int ,指針被稱之為“指向int類型的指針”。

如果指針名字為p,那麼可以說“p指針指向變量x”,或者說“p指針儲存了x變量的記憶體位址”。

同時,p 表達式對應p指針指向的變量的值。一般 p 表達式讀取指針指向的變量的值,這裡

為int類型的值,同時因為 *p 對應一個變量,是以該表達式也可以出現在指派語句的左邊,表

示更新指針所指向的變量的值。

x := 1

p := &x // p, of type *int, points to x

fmt.println(*p) // "1"

*p = 2 // equivalent to x = 2

fmt.println(x) // "2"

變量有時候被稱為可尋址的值。即使變量由表達式臨時生成,那麼表達式也必須能接受 & 取位址操作。

任何類型的指針的零值都是nil。如果 p != nil 測試為真,那麼p是指向某個有效變量。指針之間也

是可以進行相等測試的,隻有當它們指向同一個變量或全部是nil時才相等。

var x, y int

fmt.println(&x == &x, &x == &y, &x == nil) // "true false false"

在go語言中,傳回函數中局部變量的位址也是安全的。例如下面的代碼,調用f函數時建立局

部變量v,在局部變量位址被傳回之後依然有效,因為指針p依然引用這個變量。

var p = f()

func f() *int {

v := 1

return &v

因為指針包含了一個變量的位址,是以如果将指針作為參數調用函數,那将可以在函數中通

過該指針來更新變量的值。

例如下面這個例子就是通過指針來更新變量的值,然後傳回更新

後的值,可用在一個表達式中

func incr(p *int) int {

*p++ // 非常重要:隻是增加p指向的變量的值,并不改變p指針!!!

return *p

每次我們對一個變量取位址,或者複制指針,我們都是為原變量建立了新的别名。

如, *p 就是 變量v的别名。指針特别有價值的地方在于我們可以不用名字而通路一個變

量,但是這是一把雙刃劍:要找到一個變量的所有通路者并不容易,我們必須知道變量全部

的别名(譯注:這是go語言的垃圾回收器所做的工作)。

不僅僅是指針會建立别名,很多其他引用類型也會建立别名,例如slice、map和chan,甚至結

構體、數組和接口都會建立所引用變量的别名。

另一個建立變量的方法是調用用内建的new函數。表達式new(t)将建立一個t類型的匿名變

量,初始化為t類型的零值,然後傳回變量位址,傳回的指針類型為 *t 。

p := new(int) // p, *int 類型, 指向匿名的 int 變量

fmt.println(*p) // "0"

*p = 2 // 設定 int 匿名變量的值為 2

fmt.println(*p) // "2"

用new建立變量和普通變量聲明語句方式建立變量沒有什麼差別,除了不需要聲明一個臨時變

量的名字外,我們還可以在表達式中使用new(t)。

由于new隻是一個預定義的函數,它并不是一個關鍵字,是以我們可以将new名字重新定義為

别的類型。

變量的生命周期指的是在程式運作期間變量有效存在的時間間隔。

變量的生命周期指的是在程式運作期間變量有效存在的時間間隔。對于在包一級聲明的變量

來說,它們的生命周期和整個程式的運作周期是一緻的。而相比之下,在局部變量的聲明周

期則是動态的:從每次建立一個新變量的聲明語句開始,直到該變量不再被引用為止,然後

變量的存儲空間可能被回收。函數的參數變量和傳回值變量都是局部變量。它們在函數每次

被調用的時候建立。

那麼垃go語言的自動圾收集器是如何知道一個變量是何時可以被回收的呢?

基本的實作思路是,從每個包級的變量和每個目前運作函數的每一個局部變量開始,通過指

針或引用的通路路徑周遊,是否可以找到該變量。如果不存在這樣的通路路徑,那麼說明該

變量是不可達的,也就是說它是否存在并不會影響程式後續的計算結果。

因為一個變量的有效周期隻取決于是否可達,是以一個循環疊代内部的局部變量的生命周期

可能超出其局部作用域。同時,局部變量可能在函數傳回之後依然存在。

編譯器會自動選擇在棧上還是在堆上配置設定局部變量的存儲空間,但這個選擇并不是由用var還

是new聲明變量的方式決定的。

var global *int

func f() {

var x int

x = 1

global = &x

func g() {

y := new(int)

*y = 1

f函數裡的x變量必須在堆上配置設定,因為它在函數退出後依然可以通過包一級的global變量找

到,雖然它是在函數内部定義的;用go語言的術語說,這個x局部變量從函數f中逃逸了。相

反,當g函數傳回時,變量 y 将是不可達的,也就是說可以馬上被回收的。是以, y 并沒

有從函數g中逃逸,編譯器可以選擇在棧上配置設定 *y 的存儲空間,雖然這裡用的是new方式。

其實在任何時候,你并不需為了編寫正确的代碼而要考慮變量的逃逸行為,要記住的是,逃

逸的變量需要額外配置設定記憶體,同時對性能的優化可能會産生細微的影響。

go語言的自動垃圾收集器對編寫正确的代碼是一個巨大的幫助,但也并不是說你完全不用考

慮記憶體了。你雖然不需要顯式地配置設定和釋放記憶體,但是要編寫高效的程式你依然需要了解變

量的生命周期。例如,如果将指向短生命周期對象的指針儲存到具有長生命周期的對象中,

特别是儲存到全局變量時,會阻止對短生命周期對象的垃圾回收(進而可能影響程式的性

能)。

使用指派語句可以更新一個變量的值。

例子:

x = 1 // 命名變量的指派

*p = true // 通過指針間接指派

person.name = "bob" // 結構體字段指派

count[x] = count[x] * scale // 數組、slice或map的元素指派

另外,還有如下簡潔的書寫方式:

count[x] *= scale

v++ // 等價方式 v = v + 1;v 變成 2

v-- // 等價方式 v = v - 1;v 變成 1

元組指派:

元組指派是另一種形式的指派語句,它允許同時更新多個變量的值。在指派之前,指派語句

右邊的所有表達式将會先進行求值,然後再統一更新左邊對應變量的值。這對于處理有些同

時出現在元組指派語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值:

x, y = y, x

a[i], a[j] = a[j], a[i]

元組指派也可以使一系列瑣碎指派更加緊湊:

i, j, k = 2, 3, 5

但如果表達式太複雜的話,應該盡量避免過度使用元組指派;因為每個變量單獨指派語句的

寫法可讀性會更好。

有些表達式會産生多個值,比如調用一個有多個傳回值的函數。當這樣一個函數調用出現在

元組指派右邊的表達式中時(注:右邊不能再有其它表達式),左邊變量的數目必須和右

邊一緻。

f, err = os.open("foo.txt") // function call returns two values

可指派性:

指派語句是顯式的指派形式,但是程式中還有很多地方會發生隐式的指派行為:函數調用會

隐式地将調用參數的值指派給函數的參數變量,一個傳回語句将隐式地将傳回操作的值指派

給結果變量,一個複合類型的字面量也會産生指派行為。

medals := []string{"gold", "silver", "bronze"}

medals[0] = "gold"

medals[1] = "silver"

medals[2] = "bronze"

隻有右邊的值對于左邊的變量是可指派的,指派語句才是允許的。

一個類型聲明語句建立了一個新的類型名稱,和現有類型具有相同的底層結構。新命名的類

型提供了一個方法,用來分隔不同概念的類型,這樣即使它們底層類型相同也是不相容的。

type 類型名字 底層類型

類型聲明語句一般出現在包一級,是以如果新建立的類型名字的 首字元大寫 ,則在外部包也可以使用。

package tempconv

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,但是它們是不同的資料類型,是以它們不可以被互相比較或混在

一個表達式運算。

對于每一個類型t,都有一個對應的類型轉換操作t(x),用于将x轉為t類型(譯注:如果t是

指針類型,可能會需要用小括弧包裝t,比如 (*int)(0) )。隻有當兩個類型的底層基礎類型

相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換隻

改變類型而不會影響值本身。

數值類型之間的轉型也是允許的,并且在字元串和一些特定類型的slice之間也是可以轉換的;

go語言中的包和其他語言的庫或子產品的概念類似,目的都是為了支援子產品化、封裝、單獨編

譯和代碼重用。一個包的源代碼儲存在一個或多個以.go為檔案字尾名的源檔案中,通常一個

包所在目錄路徑的字尾是包的導入路徑;例如包gopl.io/ch1/helloworld對應的目錄路徑是

$gopath/src/gopl.io/ch1/helloworld

每個包都對應一個獨立的名字空間

包還可以讓我們通過控制哪些名字是外部可見的來隐藏内部實作資訊。在go語言中,一個簡

單的規則是:如果一個名字是大寫字母開頭的,那麼該名字是導出的.

包級别的名字,例如在一個檔案聲明的類型和常量,在同一個包的其他源檔案也是可以直接

通路的,就好像所有代碼都在一個檔案一樣.

在每個源檔案的包聲明前僅跟着的注釋是包注釋。包注釋的第一句應該先是包的功能概要說

明。一個包通常隻有一個源檔案有包注釋(譯注:如果有多個包注釋,目前的文檔工具會根

據源檔案名的先後順序将它們連結為一個包注釋)。如果包注釋很大,通常會放到一個獨立

的doc.go檔案中。

導入包:

在go語言程式中,每個包都是有一個全局唯一的導入路徑。

go語言的規範并沒有定義這些字元串的具體含義或包來自哪裡,它們是由建構工具來解釋的。

當使用go語言自帶的go工具箱時,一個導入路徑代表一個目錄中的一個或多個go源檔案。

除了包的導入路徑,每個包還有一個包名,包名一般是短小的名字(并不要求包名是唯一

的),包名在包的聲明處指定。按照慣例,一個包的名字和包的導入路徑的最後一個字段相

同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。

要使用gopl.io/ch2/tempconv包,需要先導入:

import (

"gopl.io/ch2/tempconv"

導入語句将導入的包綁定到一個短小的名字,然後通過該短小的名字就可以引用包中導出的

全部内容。

導入聲明将允許我們以tempconv.ctof的形式來通路gopl.io/ch2/tempconv包中的内容。

但是我們也可以綁定到另一個名稱,以避免名字沖突。

如果導入了一個包,但是又沒有使用該包将被當作一個編譯錯誤處理。這種強制規則可以有

效減少不必要的依賴,雖然在調試期間可能會讓人讨厭。

包的初始化:

包的初始化首先是解決包級變量的依賴順序,然後安照包級變量聲明出現的順序依次初始化:

var a = b + c // a 第三個初始化, 為 3

var b = f() // b 第二個初始化, 為 2, 通過調用 f (依賴c)

var c = 1 // c 第一個初始化, 為 1

func f() int { return c + 1 }

如果包中含有多個.go源檔案,它們将按照發給編譯器的順序進行初始化,go語言的建構工具

首先會将.go檔案根據檔案名排序,然後依次調用編譯器編譯。

對于在包級别聲明的變量,如果有初始化表達式則用表達式初始化,還有一些沒有初始化表

達式的,例如某些表格資料初始化并不是一個簡單的指派過程。在這種情況下,我們可以用

一個特殊的init初始化函數來簡化初始化工作。每個檔案都可以包含多個init初始化函數

func init() { / ... / }

這樣的init初始化函數除了不能被調用或引用外,其他行為和普通函數類似。在每個檔案中的

init初始化函數,在程式開始執行時按照它們聲明的順序被自動調用。

每個包在解決依賴的前提下,以導入聲明的順序初始化,每個包隻會被初始化一次。是以,

如果一個p包導入了q包,那麼在p包初始化的時候可以認為q包必然已經初始化過了。初始化

工作是自下而上進行的,main包最後被初始化。以這種方式,可以確定在main函數執行之

前,所有依然的包都已經完成初始化工作了。

一個聲明語句将程式中的實體和一個名字關聯,比如一個函數或一個變量。聲明語句的作用

域是指源代碼中可以有效使用這個名字的範圍。

不要将作用域和生命周期混為一談。聲明語句的作用域對應的是一個源代碼的 文本區域 ;它

是一個編譯時的屬性。一個變量的生命周期是指程式運作時變量存在的 有效時間段 ,在此時

間區域内它可以被程式的其他部分引用;是一個運作時的概念。

文法塊是由花括弧所包含的一系列語句,就像函數體或循環體花括弧對應的文法塊那樣。語

法塊内部聲明的名字是無法被外部文法塊通路的。

文法決定了内部聲明的名字的作用域範圍。

文法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼,

我們稱之為文法塊。有一個文法塊為整個源代碼,稱為全局文法塊;然後是每個包的包文法

決;每個for、if和switch語句的文法決;每個switch或select的分支也有獨立的文法決;當然也

包括顯式書寫的文法塊(花括弧包含的語句)。

聲明語句對應的詞法域決定了作用域範圍的大小。對于内置的類型、函數和常量,比如int、

len和true等是在全局作用域的,是以可以在整個程式中直接使用。任何在在函數外部(也就

是包級文法域)聲明的名字可以在同一個包的任何源檔案中通路的。對于導入的包,例如

tempconv導入的fmt包,則是對應源檔案級的作用域,是以隻能在目前的檔案中通路導入的

fmt包,目前包的其它源檔案無法通路在目前源檔案導入的包。還有許多聲明語句,比如

tempconv.ctof函數中的變量c,則是局部作用域的,它隻能在函數内部(甚至隻能是局部的

某些部分)通路。

控制流标号,就是break、continue或goto語句後面跟着的那種标号,則是函數級的作用域。

一個程式可能包含多個同名的聲明,隻要它們在不同的詞法域就沒有關系。

當編譯器遇到一個名字引用時,如果它看起來像一個聲明,它首先從最内層的詞法域向全局

的作用域查找。如果查找失敗,則報告“未聲明的名字”這樣的錯誤。如果該名字在内部和外部

的塊分别聲明過,則内部塊的聲明首先被找到。在這種情況下,内部聲明屏蔽了外部同名的

聲明,讓外部的聲明的名字無法被通路。

在包級别,聲明的順序并不會影響作用域範圍,是以一個先聲明的可以引用它自身或者是引

用後面的一個聲明,這可以讓我們定義一些互相嵌套或遞歸的類型或函數。

和for循環類似,if和switch語句也會在條件部分建立隐式詞法域,還有它們對應的執行體詞法域:

if x := f(); x == 0 {

fmt.println(x)

} else if y := g(x); x == y {

fmt.println(x, y)

} else {

fmt.println(x, y) // compile error: x and y are not visible here

第二個if語句嵌套在第一個内部,是以第一個if語句條件初始化詞法域聲明的變量在第二個if中

也可以通路

switch語句的每個分支也有類似的詞法域規則:條件部分為一個隐式詞法域,然

後每個是每個分支的詞法域。

用後面的一個聲明,這可以讓我們定義一些互相嵌套或遞歸的類型或函數。但是如果一個變

量或常量遞歸引用了自身,則會産生編譯錯誤。

if f, err := os.open(fname); err != nil { // compile error: unused: f

return err

f.readbyte() // compile error: undefined f

f.close() // compile error: undefined f

變量f的作用域隻有在if語句内,是以後面的語句将無法引入它,這将導緻編譯錯誤。

通常需要在if之前聲明變量,這樣可以確定後面的語句依然可以通路變量:

f, err := os.open(fname)

if err != nil {

f.readbyte()

f.close()

你可能會考慮通過将readbyte和close移動到if的else塊來解決這個問題:

if f, err := os.open(fname); err != nil {

// f and err are visible here too

但這不是go語言推薦的做法,go語言的習慣是在if中處理錯誤然後直接傳回,這樣可以確定

正常執行的語句不需要代碼縮進。