閱讀694 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Go語言與數據庫開發:01-02

接下來,開始了解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°C\n", 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函數。表達式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名字重新定義為
別的類型。


. 變量的生命周期及GC


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

變量的生命周期指的是在程序運行期間變量有效存在的時間間隔。對於在包一級聲明的變量
來說,它們的生命周期和整個程序的運行周期是一致的。而相比之下,在局部變量的聲明周
期則是動態的:從每次創建一個新變量的聲明語句開始,直到該變量不再被引用為止,然後
變量的存儲空間可能被回收。函數的參數變量和返回值變量都是局部變量。它們在函數每次
被調用的時候創建。

那麼垃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 := 1
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
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,但是它們是不同的數據類型,因此它們不可以被相互比較或混在
一個表達式運算。

對於每一個類型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)
}
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 {
return err
}
f.ReadByte()
f.Close()

你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
if f, err := os.Open(fname); err != nil {
return err
} else {
// f and err are visible here too
f.ReadByte()
f.Close()
}

但這不是Go語言推薦的做法,Go語言的習慣是在if中處理錯誤然後直接返回,這樣可以確保
正常執行的語句不需要代碼縮進。

最後更新:2017-07-27 09:03:12

  上一篇:go  阿裏雲OSS歸檔存儲類型單價下調 45%
  下一篇:go  Linux問題情報分享(2):grub-install工具不能處理/dev/xvda*路徑