一天一語言:快速入門 Go 語言
因爲在看 docker 源代碼,必須需要了解 Go 語言,所以做了一些學習和記錄,主要記錄兩者不同的地方。根據實際代碼閱讀中的問題而來,省略了和 C 語言相同的部分, 乾貨滿滿。
Go 語言定義類型和變量名,方向和一般語言是反的,這點我覺得簡直是反人類,非要搞個不一樣的顯示自己多牛牪犇
關鍵字
-
GOROOT
GO 語言安裝路徑
-
GOPATH
代碼包所在路徑, 安裝在系統上的 GO 包, 路徑爲工作區位置, 有源文件, 相關包對象, 執行文件
GO 程序結構
|-- bin 編譯後的可執行文件
|-- pkg 編譯後的包文件 (.a)
|-- src 源代碼
一般來說, bin 和 pkg 不用創建, go 命令自動創建
編譯運行
兩種方式
- 直接執行
$go run hello.go # 實際是編譯成A.OUT再執行
- 編譯執行
$go build hello.go
$./hello
關於分號
其實,和 C 一樣,Go 的正式的語法使用分號來終止語句。和 C 不同的是,這些分號由詞法分析器在掃描源代碼過程中使用簡單的規則自動插入分號,因此輸入源代碼多數時候就不需要分號了。
包
先來看一個最簡單的例子, 程序員都知道的 hello world
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
package main 就定義了包名。你必須在源文件中非註釋的第一行指明這個文件屬於哪個包,如:package main。package main 表示一個可獨立執行的程序,每個 Go 應用程序都包含一個名爲 main 的包
-
多個文件組成, 編譯後與一個文件類似, 相互可以直接引用, 因此不能有相同的全局函數和變量. 不得導入源代碼文件中沒有用到的 package,否則 golang 編譯器會報編譯錯誤
-
每個子目錄只能存在一個 package, 同一個 package 可以由多個文件組成
-
package 中每個 init() 函數都會被調用, 如果不同文件, 按照文件名字字符串比較 "從小到大" 的順序, 同一個文件從上到下
-
要生成 golang 可執行程序,必須建立一個名爲 main 的 package,並且在該 package 中必須包含一個名爲 main() 的函數。
-
import 關鍵字導入的是 package 路徑,而在源文件中使用 package 時,才需要 package 名。經常可見的 import 的目錄名和源文件中使用的 package 名一致容易造成 import 關鍵字後即是 package 名的錯覺,真正使用時,這兩者可以不同
import 特殊語法
加載自己寫的模塊:
import "./model" # 當前文件同一個目錄下的model目錄
import "url/model" # 加載GOPATH/src/url/model
點(.)操作
點(.)操作的含義是:點(.)標識的包導入後,調用該包中函數時可以省略前綴包名。
package main
import (
. "fmt"
"os"
)
func main() {
for _, value := range os.Args {
Println(value)
}
}
別名操作
別名操作的含義是:將導入的包命名爲另一個容易記憶的別名
package main
import (
f "fmt"
"os"
)
func main() {
for _, value := range os.Args {
f.Println(value)
}
}
下劃線(_)操作
下劃線()操作的含義是:導入該包,但不導入整個包,而是執行該包中的 init 函數,因此無法通過包名來調用包中的其他函數。使用下劃線()操作往往是爲了註冊包裏的引擎,讓外部可以方便地使用。
import _ "package1"
import _ "package2"
import _ "package3"
...
變量
變量定義
和 C 語言是反的
var variable_list data_type
也可以採用混合型
var a,b,c = 3,4,"foo"
:=
:= 表示聲明變量並賦值
d:=100 #系統自動推斷類型,不需要var關鍵字
特殊變量
""是特殊變量, 任何賦值給"" 的值都會被丟棄
指針
var var_name *var_type
nil 爲空指針
數組
var variable_name [size] variable_type
var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}
數組的切片,和 python 的風格
a := [5]int{1, 2, 3, 4, 5}
b := a[2:4] // a[2] 和 a[3],但不包括a[4]
fmt.Println(b)
b = a[:4] // 從 a[0]到a[4],但不包括a[4]
fmt.Println(b)
b = a[2:] // 從 a[2]到a[4],且包括a[2]
fmt.Println(b)
循環與判斷
-
if 語句沒有圓括號, 但是必須有花括號;
-
switch 沒有 break;
-
for 沒有圓括號;(注意 go 中沒有 while)
//經典的for語句 init; condition; post
for i := 0; i<10; i++{
fmt.Println(i)
}
//精簡的for語句 condition
i := 1
for i<10 {
fmt.Println(i)
i++
}
函數
func function_name( [parameter list] ) [return_types]
{
body of the function
}
函數可以有多個返回值
函數還可以輸入不定參數, 詳細用法見例子:
func sum(nums ...int) {
fmt.Print(nums, " ") //輸出如 [1, 2, 3] 之類的數組
total := 0
for _, num := range nums { //要的是值而不是下標
total += num
}
fmt.Println(total)
}
func main() {
sum(1, 2)
sum(1, 2, 3)
//傳數組
nums := []int{1, 2, 3, 4}
sum(nums...)
}
方法
一個方法就是一個包含了接受者的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指針。所有給定類型的方法屬於該類型的方法集。
語法:
func (variable_name variable_data_type) function_name() [return_type]{
/* function body*/
}
下面定義一個結構體類型和該類型的一個方法:
type User struct {
Name string
Email string
}
func (u User) Notify() error
首先我們定義了一個叫做 User 的結構體類型,然後定義了一個該類型的方法叫做 Notify,該方法的接受者是一個 User 類型的值。要調用 Notify 方法我們需要一個 User 類型的值或者指針:
// User 類型的值可以調用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()
// User 類型的指針同樣可以調用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()
注意,當接受者不是一個指針時,該方法操作對應接受者的值的副本 (意思就是即使你使用了指針調用函數,但是函數的接受者是值類型,所以函數內部操作還是對副本的操作,而不是指針操作), 當接受者是指針時,即使用值類型調用那麼函數內部也是對指針的操作
接口
Go 和傳統的面向對象的編程語言不太一樣, 沒有類和繼承的概念. 通過接口來實現面向對象.
語法:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
實現某個接口的類型,除了實現接口的方法外,還可以有自己的方法。
package main
import "fmt"
type Shaper interface {
Area() float64
// Perimeter() float64
}
type Rectangle struct {
length float64
width float64
}
// 實現 Shaper 接口中的方法
func (r *Rectangle) Area() float64 {
return r.length * r.width
}
// Set 是屬於 Rectangle 自己的方法
func (r *Rectangle) Set(l float64, w float64) {
r.length = l
r.width = w
}
func main() {
rect := new(Rectangle)
rect.Set(2, 3)
areaIntf := Shaper(rect)
fmt.Printf("The rect has area: %f\n", areaIntf.Area())
}
如果去掉 Shaper 中 Perimeter() float64 的註釋,編譯的時候報錯誤,這是因爲 Rectangle 沒有實現 Perimeter() 方法。
- 多個類型可以實現同一個接口。
- 一個類型可以實現多個接口。
內存分配
有 new 和 make
-
new:new(T) 爲一個類型爲 T 的新項目分配了值爲零的存儲空間並返回其地址,也就是一個類型爲 * T 的值, 返回了一個指向新分配的類型爲 T 的零值的指針, 內存只是清零但是沒有初始化.
-
make: 僅用於創建切片、map 和 chan(消息管道),並返回類型 T(不是 * T)的一個被初始化了的(不是零)實例。
錯誤處理 – Defer
爲了進行錯誤處理, 比如防止資源泄露, go 設計了一個 defer 函數
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
Go 的 defer 語句預設一個函數調用(延期的函數),該調用在函數執行 defer 返回時立刻運行。該方法顯得不同常規,但卻是處理上述資源泄露情況很有效,無論函數怎樣返回,都必須進行資源釋放。
再看一個列子
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
被延期的函數以後進先出(LIFO)的順行執行,因此以上代碼在返回時將打印 4 3 2 1 0
協程 goroutine
先來複習下, 進程, 線程和協程的概念, GoRoutine 就是 Go 的協程
-
進程:分配完整獨立的地址空間,擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程的切換隻發生在內核態,由操作系統調度。
-
線程:和其它本進程的線程共享地址空間,擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程的切換一般也由操作系統調度 (標準線程是的)。
-
協程:和線程類似,共享堆,不共享棧,協程的切換一般由程序員在代碼中顯式控制。
goroutine 基本概念
GoRoutine 主要是使用 go 關鍵字來調用函數,你還可以使用匿名函數;
注意, go routine 被調度的的先後順序是沒法保證的
package main
import "fmt"
func f(msg string) {
fmt.Println(msg)
}
func main(){
go f("goroutine")
go func(msg string) {
fmt.Println(msg)
}("going")
}
下面來看一個常見的錯誤用法
array := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}
var i = 0
for index, item := range array {
go func() {
fmt.Println("index:", index, "item:", item)
i++
}()
}
time.Sleep(time.Second * 1)
fmt.Println("------------------")
//output:
------------------
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
------------------
最初的意圖是 index 與 item 每次爲 1,a;2,b;3,c;…. 這樣, 結果卻不是這樣, 到底什麼原因呢?
這裏的 go func 每個 index 與 item 是共享的,並不是局部的,由於 for 循環的執行是很快的,每次循環啓動一個 go routine,在 for 循環結束之後(此時 index 與 item 的值分別變成了 8 與 e),但是這個時候第一個啓動的 goroutine 可能還沒有開始執行,由於它們是共享變量的,之後所有輸出的 index 與 item 都是 8 與 e 於是出現了上面的效果。
將原來程序做些修改就能滿足要求了:
for i = 0; i < length; i++ {
go func(index int) {
//這裏如果打印 array[i]的話 就會index out of range了因爲i是全局的(在執行到打印語句的時候i的值已經變成了length+1了)不是新啓動的這個goroutine的
//新啓動的goroutine與原來的main routine 是共享佔空間的 因此 這個i也是共享的
fmt.Println("index:", index, "item:", array[index])
}(i)
goroutine 併發
goroutine 有個特性,如果一個 goroutine 沒有被阻塞,那麼別的 goroutine 就不會得到執行, 這並不是真正的併發,如果你要真正的併發,你需要在你的 main 函數的第一行加上下面的這段代碼:
import "runtime"
...
runtime.GOMAXPROCS(4)
goroutine 併發安全性問題, 需要注意:
- 互斥量上鎖
var mutex = &sync.Mutex{} //可簡寫成:var mutex sync.Mutex
mutex.Lock()
...
mutex.Unlock()
- 原子性操作
import "sync/atomic"
......
atomic.AddUint32(&cnt, 1)
......
cntFinal := atomic.LoadUint32(&cnt)//取數據
Channel 信道
Channel 的基本概念
Channal 就是用來通信的,像 Unix 下的管道一樣,
它的操作符是箭頭 "<-" , 箭頭的指向就是數據的流向
ch <- v // 發送值v到Channel ch中
v := <-ch // 從Channel ch中接收數據,並將數據賦值給v
下面的程序演示了一個 goroutine 和主程序通信的例程。
package main
import "fmt"
func main() {
//創建一個string類型的channel
channel := make(chan string)
//創建一個goroutine向channel裏發一個字符串
go func() { channel <- "hello" }()
msg := <- channel
fmt.Println(msg)
}
chan 爲先入先出的隊列, 有三種類型, 雙向, 只讀, 只寫, 分別爲 "chan","chan<-","<-chan"
初始化時候, 可以指定容量make(chanint,100)
; 容量 (capacity) 代表 Channel 容納的最多的元素的數量
Channel 的阻塞
channel 默認上是阻塞的,也就是說,如果 Channel 滿了,就阻塞寫,如果 Channel 空了,就阻塞讀。於是,我們就可以使用這種特性來同步我們的發送和接收端。
package main
import "fmt"
import "time"
func main() {
channel := make(chan string) //注意: buffer爲1
go func() {
channel <- "hello"
fmt.Println("write \"hello\" done!")
channel <- "World" //Reader在Sleep,這裏在阻塞
fmt.Println("write \"World\" done!")
fmt.Println("Write go sleep...")
time.Sleep(3*time.Second)
channel <- "channel"
fmt.Println("write \"channel\" done!")
}()
time.Sleep(2*time.Second)
fmt.Println("Reader Wake up...")
msg := <-channel
fmt.Println("Reader: ", msg)
msg = <-channel
fmt.Println("Reader: ", msg)
msg = <-channel //Writer在Sleep,這裏在阻塞
fmt.Println("Reader: ", msg)
}
結果爲
Reader Wake up...
Reader: hello
write "hello" done!
write "World" done!
Write go sleep...
Reader: World
write "channel" done!
Reader: channel
多個 Channel 的 select
package main
import "time"
import "fmt"
func main() {
//創建兩個channel - c1 c2
c1 := make(chan string)
c2 := make(chan string)
//創建兩個goruntine來分別向這兩個channel發送數據
go func() {
time.Sleep(time.Second * 1)
c1 <- "Hello"
}()
go func() {
time.Sleep(time.Second * 1)
c2 <- "World"
}()
//使用select來偵聽兩個channel
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
定時器
Go 語言中可以使用 time.NewTimer 或 time.NewTicker 來設置一個定時器,這個定時器會綁定在你的當前 channel 中,通過 channel 的阻塞通知機器來通知你的程序。
package main
import "time"
import "fmt"
func main() {
timer := time.NewTimer(2*time.Second)
<- timer.C
fmt.Println("timer expired!")
}
關閉 channel
使用 close 命令
close(channel)
系統調用
Go 語言主要是通過兩個包完成的。一個是 os 包,一個是 syscall 包。
這兩個包裏提供都是 Unix-Like 的系統調用,
-
syscall 裏提供了什麼 Chroot/Chmod/Chmod/Chdir…,Getenv/Getgid/Getpid/Getgroups/Getpid/Getppid…,還有很多如 Inotify/Ptrace/Epoll/Socket/… 的系統調用。
-
os 包裏提供的東西不多,主要是一個跨平臺的調用。它有三個子包,Exec(運行別的命令), Signal(捕捉信號)和 User(通過 uid 查 name 之類的)
如執行命令行
package main
import "os/exec"
import "fmt"
func main() {
cmd := exec.Command("ping", "127.0.0.1")
out, err := cmd.Output()
if err!=nil {
println("Command Error!", err.Error())
return
}
fmt.Println(string(out))
}
作者:Chenzongshu
鏈接:https://www.jianshu.com/p/fe16d3762043
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/htQ4_HMxka8OBCBcBmgpqw