一個在線的Go編譯器
如果還沒來得及安裝Go環境,想體驗一下Go語言,可以在Go在線編譯器 上運行Go程序。
格式化
讓所有人都遵循一樣的編碼風格是一種理想,現在Go語言通過gofmt程序
,讓機器來處理大部分的格式化問題。gofmt程序
是go標準庫提供的一段程序,可以嘗試運行它,它會按照標準風格縮進,對齊,保留註釋,它默認使用制表符進行縮進。Go標準庫的所有代碼都經過gofmt程序
格式化的。
註釋
Go註釋支持C風格的塊註釋/* */
和C++風格的行註釋//
。塊註釋主要用作包的註釋。Go官方提倡每個包都應包含一段包註釋,即放置在包子句前的一個塊註釋。對於有多個文件的包,包註釋只需要出現在其中一個文件即可。
godoc 既是一個程序,又是一個 Web 服務器,它對 Go 的源碼進行處理,並提取包中的文檔內容。 出現在頂級聲明之前,且與該聲明之間沒有空行的註釋,將與該聲明一起被提取出來,作為該條目的說明文檔。
命名
- Go語言的命名會影響語義:某個名稱在包外是否可見,取決於其首個字符是否為大寫字母。
- 包:應當以小寫的單個單詞來命名,且不應使用下劃線或駝峰記法。
- 包名:應為其源碼目錄的基本名稱,例如在 src/pkg/encoding/base64 中的包應作為"encoding/base64" 導入,其包名應為 base64
- 獲取器:若有個名為 owner (小寫,未導出) 的字段,其獲取器應當名為 Owner(大寫,可導出) 而非 GetOwner。若要提供設置器方法,可以選擇SetOwner。
- 接口:只包含一個方法的接口應當以該方法的名稱加上 - er 後綴來命名
- 駝峰記法:Go 中約定使用駝峰記法 MixedCaps 或 mixedCaps
分號
- Go的詞法分析器會用簡單的規則來自動插入分號
- 如果在一行中寫多個語句,需要用分號隔開
- 控制結構的左大括號不能放在下一行,因為根據詞法分析器的規則,會在大括號前加入一個分號,造成錯誤
初始化
常量必須在定義的時候就進行初始化。常量只能是數字、字符、字符串、布爾值等基本類型,定義它們的表達式必須是在編譯期就可以求值的類型。使用const
來定義一個常量:
const LENGTH int = 10
const WIDTH int = 5
在Go中,枚舉常量
使用iota
來創建,iota
是一個自增長的值:
type AudioOutput int
const (
OutMute AudioOutput = iota // 0
OutMono // 1
OutStereo // 2
_
_
OutSurround // 5
)
iota
總是用於increment,但它也可以用於表達式,在《effective Go》展示了一個定義數量級的表示:
type ByteSize float64
const (
_ = iota // 使用_來忽略iota=0
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
源文件可以定義無參數init函數
,該函數在真正執行函數邏輯之前被自動調用,下面的程序簡單說明這一點:
package main
import "fmt"
func init() {
fmt.Print("執行init函數0\n")
}
func init() {
fmt.Print("執行init函數1\n")
}
func init() {
fmt.Print("執行init函數2\n")
}
func main() {
fmt.Print("執行main函數\n")
}
//output :
執行init函數0
執行init函數1
執行init函數2
執行main函數
可以看到,在執行main函數中的邏輯前,init函數會先被調用,而且同一個源文件中可以定義多個init函數。init函數通常被用在程序真正執行之前對變量、程序狀態進行校驗。它的執行機制是這樣的:
- 該包中所有的變量都被初始化器求值後,init才會被調用
- 之後在所有已導入的包都被初始化之後,init才會被調用
控制結構
Go使用更加通用的for來代替do與while循環,for的三種形式為:
// Like a C for
for init ; condition;post { }
//Like a C while
for condition{ }
//Like a C for(;;)
for {}
對於數組、切片、字符串、map,或者從信道讀取消息,可以使用range子句
:
for key ,value := range oldMap {
newMap[key] = value
}
Go的switch要更加靈活通用,當switch
後面沒有表達式的時候,它將匹配ture
,這也意味著if-else-if-else
鏈可以使用switch
來實現:
func unhex(c byte) byte {
switch { //switch將匹配true
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
函數
Go的函數可以進行多值返回。在C語言中經常有這種笨拙的用法:函數通過返回值來告知函數的執行情況,例如返回0代表無異常,返回-1表示EOF
等,而通過指針實參來傳遞數據給外部。現在使用Go函數的多值返回可以解決解決這個問題。下面是Go標準庫中打開文件的File.Write
的簽名:
func (file *File) Write(b []byte) (n int, err error)
Write
函數返回寫入的字節數以及一個錯誤。如果正確寫入了,則err
為nil
,否則,err
為一個非nil
的error
錯誤值,這在Go中是一種常見的編碼風格。
Go函數的返回值可以被命名。Go的返回值在函數體內可以作為常規的變量來使用,稱為結果“形參”
,結果“形參”
在函數開始執行時被初始化與其類型相應的零值。如果函數執行了不帶參數的return
,則把結果形參的當前值返回:
func abs(i int) (result int){
if i < 0{
result = -i //返回值result可以直接當成常規變量使用
}
return
}
這樣做的好處是函數的簽名即為文檔,返回值的含義也寫到了函數簽名中,提高了代碼的可讀性。
Go提供defer語句用於延遲執行函數。defer語句修飾的函數,在外層函數結束之前被調用。可以這樣來使用defer語句
:
func printStr (a string){
fmt.Print(a);
}
func main() {
defer printStr("one\n")
defer printStr("two\n")
defer printStr("three\n")
fmt.Print("main()\n")
}
//output :
main()
three
two
one
關於defer語句
:
- 適用於關閉打開的文件,避免多個返回路徑都需要去關閉文件。
- 被推遲執行的函數的實參,才推遲執行時就會求值,而不是在調用執行時才求值。
- 被推遲的函數按照後進先出(LIFO)的順序執行。
- defer語句是在函數級別的,即使把它寫在大括號(塊)中,也只會在調用函數結束時才調用被推遲執行的函數。
使用defer語句
時還有一些細節需要註意。下面這段代碼:
func main() {
fmt.Print(test())
}
func test() (r int) {
defer func() {
r = 1
return
}()
r = 2
return 3
}
//output:
1
輸出並不是3
,而是1
.原因是return
的操作實際包括了:
r = 0 //結果“形參”在函數開始執行時被初始化為零值
r = 2
r = 1 //defer語句執行
return r
內存分配
Go提供了兩種分配原語new
與make
:
func new(Type) *Type
func make(t Type, size ...IntegerType) Type
new(T)
用於分配內存,它返回一個指針,指向新分配的,類型為T的零值,通過new
來申請的內存都會被置零。這意味著如果設計了某種數據結構,那麽每種類型的零值就不必進一步初始化了。
make(T,args)
的目的不同於new(T)
,它只用於創建切片(slice)、映射(map)、信道(channel),這三種類型本質上與引用數據類型,它們在使用前必須初始化。make
返回類型為一個類型為T
的已初始化的值,而非*T
。
下面是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)
數組
Go的數組與C語言的數組有很大的區別:
- 數組是值,把數組傳遞給函數,函數會得到該數組的一個副本,而不是指針。
- 數組的大小是類型的一部分。
[10]int
與[20]int
是兩種類型。
如果想要像C語言那樣傳遞數組指針,需要這樣做:
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
} r
eturn
} a
rray := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // 註意顯式的取址操作
但在Go中通常不會這樣做,而是通過切片來實現引用的傳遞。切片保存了對底層數組的引用,若你將某個切片賦予另一個切片,它們會引用同一個數組。
切片
切片是一個很小的對象,它對底層數組進行了抽象,並提供相應的操作方法,切片包含3個字段,其的內部實現為:
可以通過一些方式來定義切片:
var slice0 []type //通過聲明一個未指定大小的數組來定義切片
var slice1 []type = make([]type, len) //通過make來創建切片,長度與容量都是5個元素
make([]T, length, capacity) //可以通過make來指定它的容量
聲明的時候,只要在[]
運算符裏指定了一個值,那麽創建的就是數組而不是切片,只有不指定值的時候,才會創建切片。
切片之所以稱為切片,是因為創建一個新的切片就是把底層數組切出一部分,例如代碼:
slice := [] int {10,20,30,40,50} //創建一個切片,長度與容量都是5
newSlice := slice[1:3] //創建一個新切片,其長度為5,容量為4
對底層數組容量是k的新切片slice[i,j]
來說,長度是j-i
,容量是k-i
,創建的新切片內部實現為:
由於兩個切片共享一部分的底層數組,所以修改newSlice的第2個元素,也將同樣修改了slice的第三個元素。
可以使用append
來增長切片的長度,這有兩種情況:
- 當切片的可用容量足夠時,append函數會增加切片的長度,而不會改變容量
- 當切片的可用容量不足時,append函數會增加切片的容量,增加的策略是:切片容量小於1000時,總是成倍地增加容量;一旦元素個數超過1000個,容量增加因子為1.25,也就是每次會增加25%。
當append
函數造成切片容量拓展時,該切片將擁有一個全新的底層數組。
映射
映射與切片一樣,也是引用類型。如果通過一個不存在的key
來獲取value
,將返回與該映射中項的類型對應的零值:
var map1 map[string] int
map1 = make(map[string]int ,10)
map1["one"]=1
map1["two"]=2
fmt.Print(map1["three"])
//output:
0
如果map1["three"]
的value
剛好是0,該怎麽區分呢?可以采用多重賦值
的形式來分辨這種情況:
i, ret := map1["three"]
if ret == true{
fmt.Print("map1[\"three\"]存在,值為:", i)
} else {
fmt.Print("map1[\"three\"] 不存在\n")
}
或者這樣寫更好一些,《effective Go》稱為the “comma ok” idiom
,逗號OK慣用法
if i, ret := map1["three"] ;ret {
fmt.Print("map1[\"three\"]存在,值為:", i)
} else {
fmt.Print("map1[\"three\"] 不存在\n")
}
如果僅是需要判斷某個key是否存在,可以用空白標識符_
來代替value
:
if _, ret := map1["three"] ;ret {
fmt.Print("map1[\"three\"]存在\n")
} else {
fmt.Print("map1[\"three\"]不存在,值為:")
}
使用內建函數delete
函數來刪除鍵值對,即使對應的鍵不在該映射中,delete操作也是安全的
方法
在函數的一節中,我們已經看到了write
函數的聲明為:
func (file *File) Write(b []byte) (n int, err error)
我們可以抽象出Go中函數的結構為:
func [(p mytype)] funcname([pram type]) [(parm type)] {//body}
其中,函數的(p mytype)
為可選部分,具備此部分的函數稱為方法(method)
,這部分稱為接收者(receiver)
。我們可以為任何已命名的類型,包括自己定義的結構體類型,定義方法。通過receiver
,把方法綁定到類型上。下面是一個示例:
package main
import "fmt"
//定義一個矩形類型
type rect struct {
width ,height int
}
//這個方法擴大矩形邊長為multiple倍
//這個方法的reciever為*rect
//表示這是定義在rect結構體上的方法
func (r *rect) bigger(multiple int){
r.height *=multiple
r.height *=multiple
}
//方法的reciever可以為結構體類型
//也可以為結構體指針類型
//區別在於當reciever為類型指針時
//可以在該方法內部修改結構體成員
func (r rect) area() int{
return r.width*r.height
}
func main(){
r := rect{width:10,height:5}
fmt.Print("r 's area:",r.area(),"\n")
r.bigger(10)
fmt.Print("r's area:",r.area())
}
//output:
r 's area:50
r's area:5000
以指針或值作為reciever的區別在於::
- 指針可以修改接收者
- 值方法可通過指針和值調用,而指針方法只能通過指針來調用
值方法可以通過指針和值調用,所以下面語句是合法的:
func main(){
r := rect{width:10,height:5}
//通過指針調用
fmt.Print("r 's area:",(&r).area(),"\n")
//通過值調用
fmt.Print("r 's area:",r.area(),"\n")
}
//output:
r 's area:50
r 's area:50
而對於指針方法只能通過指針來調用,你可能會感到疑惑,因為下面的語句也是合法的:
func main(){
r := rect{width:10,height:5}
fmt.Print("r 's area:",r.area(),"\n")
//通過值來調用指針方法(為什麽合法?)
r.bigger(10)
fmt.Print("r's area:",r.area())
}
//output:
其實是這樣的:如果值是可以尋址的,那麽Go會自動插入取址操作符來對付一般的通過值調用的指針方法。在這個例子中,r
是可尋址的,因此r.Bigger(10)
將被編譯器改寫為(&r).Bigger
。
另外,方法也可以"轉換"為函數,這一點便不在這裏詳談。
接口
通過方法與接口,Go語言定義了一種與Java/C++等OOP語言截然不同的“繼承”的形態。通過實現接口定義的方法,便可將reciever
的類型變量賦值給接口類型變量,通過接口類型變量來調用到reciever
類型的方法,用C++來類比,就是通過父類指針來調用到了派生類的成員函數(不過Go沒有這些概念)。下面是一個示例:
package main
import (
"fmt"
"math"
)
//定義了一個接口geometry表示幾何類型
type geometry interface {
area() float64
bigger(float64)
}
//矩形和圓形要實現這接口的兩個方法
type rect struct {
width, height float64
}
type circle struct {
radius float64
}
//在Go中,實現接口,只需要實現該接口定義的所有方法即可
//矩形的接口方法實現
func (r *rect) bigger(multiple float64) {
r.height *= multiple
r.height *= multiple
}
func (r *rect) area() float64 {
return r.width * r.height
}
//圓形的接口方法實現
func (c *circle) bigger(multiple float64){
c.radius *= multiple
}
func (c *circle) area() float64 {
return math.Pi * c.radius * c.radius
}
//可以把rect和circle類型的變量作為實參
//傳遞給geometry接口類型的變量
func measure (g geometry){
fmt.Print("geometry 's area:",g.area(),"\n")
g.bigger(2)
fmt.Print("after bigger 2 multiple, area :",g.area(),"\n")
}
func main() {
r := rect{width: 10, height: 5}
c := circle{radius:3}
measure(&r)
measure(&c)
}
//output:
geometry 's area:50
after bigger 2 multiple, area :200
geometry 's area:28.274333882308138
after bigger 2 multiple, area :113.09733552923255
類型轉換
字面量的值,Go編譯器會進行隱式轉換:
func main() { var myInt int32 =5 var myFloat float64 = 6 fmt.Print(myInt,"\n") fmt.Print(myFloat) }
這裏的
6
為整型類型的字面值常量 Integer literals.。它賦值給了float64
類型變量,編譯器進行了隱式類型轉換。底層類型不同的變量,需要顯式類型轉換:
func main() {
var myInt int32 =5
//var myFloat float64 = myInt //error
var myFloat float64 = float64(myInt) //需要顯式轉換
fmt.Print(myInt,"\n")
fmt.Print(myFloat)
}
這裏還要區分靜態類型
與底層類型
:
type IntA int32
type IntB int32
func main() {
var a IntA =1
//var b IntB = a //error
var b IntB = IntB(a)
fmt.Print(a,"\n")
fmt.Print(b)
}
這裏IntA
為變量a的靜態類型,而int32
為變量a的底層類型。即使兩個類型的底層類型相同,在相互賦值時還是需要強制類型轉換的。
接口類型變量的類型轉換,有兩種情況:
- 普通類型向接口類型的轉換:隱式進行
- 接口類型向普通類型的轉換:需要類型斷言
根據Go 官方文檔 所說,所有的類型,都實現了空接口interface{}
,所以普通類型都可以向interface{}
進行類型轉換:
func main() {
var x interface{} = "hello" // 字符串常量->interface{}
var y interface{} = []byte{'w','o','r','l','d'} //[]byte ->interface{}
fmt.Print(x," ")
fmt.Printf("%s",y)
}
而接口類型向普通類型的轉換,則需要由Comma-ok斷言
或switch測試
來進行了。
Comma-ok斷言
語法: value,ok := element.(T)
element必須為ingerface類型,斷言失敗,ok為false,否則為true,下面是例程:
func main() {
var vars []interface{} = make([]interface{},5)
vars[0] = "one"
vars[1] = "two"
vars[2] = "three"
vars[3] = 10
vars[4] = []byte{'a', 'b', 'c'}
for index, element := range vars {
if value, ok := element.(int); ok {
fmt.Printf("vars[%d] type is int,value is %d \n",index,value)
}else if value,ok := element.(string);ok{
fmt.Printf("vars[%d] type is string,value is %s \n",index,value)
}else if value,ok := element.([]byte);ok{
fmt.Printf("vars[%d] type is []byte,value is %s \n",index,value)
}
}
}
//output:
vars[0] type is string,value is one
vars[1] type is string,value is two
vars[2] type is string,value is three
vars[3] type is int,value is 10
vars[4] type is []byte,value is abc
Comma-ok斷言也可以這樣使用:
value := element.(T)
但一旦斷言失敗將產生運行時錯誤,不推薦使用。
switch測試
switch測試只能在switch語句中使用。將上面的例程改為switch測試:
func main() {
var vars []interface{} = make([]interface{}, 5)
vars[0] = "one"
vars[1] = "two"
vars[2] = "three"
vars[3] = 10
vars[4] = []byte{'a', 'b', 'c'}
for index, element := range vars {
switch value := element.(type) {
case int:
fmt.Printf("vars[%d] type is int,value is %d \n", index, value)
case string:
fmt.Printf("vars[%d] type is string,value is %s \n", index, value)
case []byte:
fmt.Printf("vars[%d] type is []byte,value is %s \n", index, value)
}
}
}
(完)
Tags: 註釋 程序 駝峰 一個 命名 格式化
文章來源: