最近寫了不少Go代碼,但是寫着寫着,還是容易忘,尤其是再寫點Python代碼後。是以找了一篇不錯的Golang基礎教程,翻譯一下,時常看看。
原文連結: 「Learning Go — from zero to hero」 by Milap Neupane
開始
Go是由各種 包 組成的。main包是程式的入口,由它告訴編譯器,這是一個可執行程式,而不是共享包。main包定義如下:
package main
工作區
Go的工作區是由環境變量
GOPATH
決定的。
你可以在工作區裡随心所欲地寫代碼,Go會在
GOPATH
或者
GOROOT
目錄下搜尋包。注:
GOROOT
是Go的安裝路徑。
設定
GOPATH
為你想要的目錄:
# export 環境變量
export GOPATH=~/workspace
# 進入工作區目錄
cd ~/workspace
在工作區目錄裡建立
mian.go
檔案。
package main
import (
"fmt"
)
func main(){
fmt.Println("Hello World!")
}
我們使用
import
關鍵字來引入一個包。
func main
是執行代碼的入口,fmt是Go的内置包,主要用來格式化輸入/輸出。而Println是fnt中的一個列印函數。
想要運作Go程式,有兩種方法。
方法一
大家都知道,Go是一門編譯型語言,是以在執行之前,我們需要先編譯它。
> go build main.go
這個指令會生成二進制可執行檔案 main,然後我們再運作它。
> ./main
# Hello World!
方法二
一個
go run
指令就可以搞定。
go run main.go
# Hello World!
注意:你可以在這個網站執行本文中的代碼。
變量
Go中的變量都是顯式聲明的。Go是靜态語言,是以聲明變量時,就會去檢查變量的類型。
變量聲明有以下三種方式。
# 1) a的預設值為0
var a int
# 2) 聲明并初始化a,a自動指派為int
var a = 1
# 3) 簡寫聲明
message := "hello world"
還可以在一行聲明多個變量
var b, c int = 2, 3
資料類型
數字,字元串 和 布爾型
Go 支援的數字存儲類型有很多,比如
int
,
int8
,
int16
,
int32
,
int64
,
uint
,
uint8
,
uint16
,
uint32
,
uint64
,
uintptr
等等。
字元串類型存儲一個位元組序列。使用
string
關鍵字來聲明。
布爾型使用
bool
聲明。
Go還支援複數類型資料類型,可以使用
complex64
和
complex128
進行聲明。
var a bool = true
var b int = 1
var c string = 'hello world'
var d float32 = 1.222
var x complex128 = cmplx.Sqrt(-5 + 12i)
數組, 分片 和 映射Map
數組是包含同一資料類型的元素序列,在聲明時确定數組長度,是以不能随意擴充。
數組的聲明方式如下:
var a [5]int
多元數組的聲明方式如下:
var multiD [2][3]int
Go中的數組有一定限制,比如不能修改數組長度、不能添加元素、不能擷取子數組。這時候,更适合使用
slice[分片]
這一類型。
分片用于存儲一組元素,允許随時擴充其長度。分片的聲明類似數組,隻是去掉了長度聲明。
var b []int
這行代碼會建立一個 0容量、0長度的分片。也可以使用以下代碼 設定分片的容量和長度。
// 初始化一個長度為5,容量為10的分片
numbers := make([]int,5,10)
實際上,分片是對數組的抽象。分片使用數組作為底層結構。一個分片由三部分組成:容量、長度和指向底層數組的指針。
使用
append
或者
copy
方法可以擴大分片的容量。
append
方法在分片的末尾追加元素,必要時會擴大分片容量。
numbers = append(numbers, 1, 2, 3, 4)
還可以使用
copy
方法來擴大容量。
// 建立一個更大容量的分片
number2 := make([]int, 15)
// 把原分片複制到新分片
copy(number2, number)
如何建立一個分片的子分片呢?參考以下代碼。
// 建立一個長度為4的分片
number2 = []int{1,2,3,4}
fmt.Println(numbers) // -> [1 2 3 4]
// 建立子分片
slice1 := number2[2:]
fmt.Println(slice1) // -> [3 4]
slice2 := number2[:3]
fmt.Println(slice2) // -> [1 2 3]
slice3 := number2[1:4]
fmt.Println(slice3) // -> [2 3 4]
Map也是Go的一種資料類型,用于記錄鍵值間的映射關系。使用以下代碼建立一個map。
var m map[string]int
// 新增 鍵/值
m['clearity'] = 2
m['simplicity'] = 3
// 列印值
fmt.Println(m['clearity']) // -> 2
fmt.Println(m['simplicity']) // -> 3
這裡,m是一個鍵為string,值為int的map變量。
類型轉換
接下來看一下如何進行簡單的類型轉換。
a := 1.1
b := int(a)
fmt.Println(b)
//-> 1
并非所有的資料類型都能轉換成其他類型。注意:確定資料類型與轉換類型互相相容。
條件語句
if else
參考以下代碼中的if-else語句進行條件判斷。注意:花括号與條件語句要在同一行。
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
switch case
switch-case用于組織多個條件語句,詳看以下代碼
i := 2
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
default:
fmt.Println("none")
}
循環
Go中用于循環的關鍵字隻有一個
for
。
i := 0
sum := 0
for i < 10 {
sum += 1
i++
}
fmt.Println(sum)
以上代碼類似于C語言中的
while
循環。另一種循環方式如下:
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
Go中的死循環
for {
}
指針
Go提供了指針,用于存儲值的位址。指針使用
*
來聲明。
var ap *int
這裡的ap變量即指向整型的指針。使用
&
運算符擷取變量位址,
*
運算符用來擷取指針所指向的值。
a := 12
ap = &a
fmt.Println(*ap)
// => 12
以下兩種情況,通常優先選用指針。
- 把結構體作為參數傳遞時。因為值傳遞會耗費更多記憶體。
- 聲明某類型的方法時。傳遞指針後,方法/函數可以直接修改指針所指向的值。
比如:
func increment(i *int) {
*i++
}
func main() {
i := 10
increment(&i)
fmt.Println(i)
}
//=> 11
函數
main
包中的
main
函數是go程式執行的入口,除此以外,我們還可以定義其他函數。
func add(a int, b int) int {
c := a + b
return c
}
func main() {
fmt.Println(add(2, 1))
}
//=> 3
如上所示,Go中使用
func
關鍵字加上函數名來定義一個函數。函數的參數需要指明資料類型,最後是傳回的資料類型。
函數的傳回值也可以在函數中提前定義:
func add(a int, b int) (c int) {
c = a + b
return
}
func main() {
fmt.Println(add(2, 1))
}
//=> 3
這裡c被定義為傳回值,是以調用
return
語句時,c會被自動傳回。
你也可以一次傳回多個變量:
func add(a int, b int) (int, string) {
c := a + b
return c, "successfully added"
}
func main() {
sum, message := add(2, 1)
fmt.Println(message)
fmt.Println(sum)
}
方法、結構體和接口
Go 不是完全面向對象的語言,但是有了 方法、結構體和接口,它也可以達到面向對象的效果。
Struct 結構體
結構體包含不同類型的字段,可用來對資料進行分組。例如,如果我們要對Person類型的資料進行分組,那麼可以定義一個人的各種屬性,包括姓名,年齡,性别等。
type person struct {
name string
age int
gender string
}
有了Person類型後,現在來建立一個 Person對象:
//方法 1: 指定參數和值
p = person{name: "Bob", age: 42, gender: "Male"}
//方法 2: 僅指定值
person{"Bob", 42, "Male"}
可以使用
.
來擷取一個對象的參數。
p.name
//=> Bob
p.age
//=> 42
p.gender
//=> Male
也可以通過結構體的指針對象來擷取參數。
pp = &person{name: "Bob", age: 42, gender: "Male"}
pp.name
//=> Bob
方法
方法是一種帶有接收器的函數。接收器可以是一個值或指針。我們可以把剛剛建立的Person類型作為接收器來建立方法:
package main
import "fmt"
// 定義結構體
type person struct {
name string
age int
gender string
}
// 定義方法
func (p *person) describe() {
fmt.Printf("%v is %v years old.", p.name, p.age)
}
func (p *person) setAge(age int) {
p.age = age
}
func (p person) setName(name string) {
p.name = name
}
func main() {
pp := &person{name: "Bob", age: 42, gender: "Male"}
// 使用 . 來調用方法
pp.describe()
// => Bob is 42 years old
pp.setAge(45)
fmt.Println(pp.age)
//=> 45
pp.setName("Hari")
fmt.Println(pp.name)
//=> Bob
}
注意,此處的接收器是一個指針,方法中對指針進行的任何修改,都可以反映在接收器
pp
上。這樣可以避免複制帶來的記憶體消耗。
注意:上面示例中,
age
被修改了,而
name
不變。因為隻有
setAge
傳入的是指針類型,可以對接收器進行修改。
接口
在Go中,接口是方法的集合。接口可以對一個類型的屬性進行分組,比如:
type animal interface {
description() string
}
animal
是一個接口。通過實作
animal
接口,我們來建立兩種不同類型的動物。
package main
import (
"fmt"
)
type animal interface {
description() string
}
type cat struct {
Type string
Sound string
}
type snake struct {
Type string
Poisonous bool
}
func (s snake) description() string {
return fmt.Sprintf("Poisonous: %v", s.Poisonous)
}
func (c cat) description() string {
return fmt.Sprintf("Sound: %v", c.Sound)
}
func main() {
var a animal
a = snake{Poisonous: true}
fmt.Println(a.description())
a = cat{Sound: "Meow!!!"}
fmt.Println(a.description())
}
//=> Poisonous: true
//=> Sound: Meow!!!
在main函數中,我們建立了一個類型為animal的變量a。然後,給動物指定蛇和貓的類型,并列印
a.description
。
包
在Go中,所有的代碼都寫在包裡面。
main
包是程式執行的入口,Go自帶了很多内置包,最有名的就是剛剛用過的
fmt
包。
“Go packages in the main mechanism for programming in the large that go provides and they make possible to divvy up a large project into smaller pieces.”
— Robert Griesemer
安裝一個包
go get <package-url-github>
// 舉個栗子
go get github.com/satori/go.uuid
包預設安裝在
GOPATH
環境變量設定的工作區中。可以使用
cd $GOPATH/pkg
指令進入目錄,檢視已安裝的包。
自定義包
首先建立一個
custom_package
檔案夾
> mkdir custom_package
> cd custom_package
假設要建立一個
person
包,首先在
custom_package
目錄下建立一個
person
檔案夾。
> mkdir person
> cd person
然後建立一個
person.go
檔案
package person
func Description(name string) string {
return "The person name is: " + name
}
func secretName(name string) string {
return "Do not share"
}
現在需要安裝這個包,以便引入并使用它。
> go install
注意:如果以上指令報錯,确認一下 GO111MODULE
環境變量是否設定正确, 參考連結。
然後回到
custom_package
目錄下,建立一個
main.go
檔案。
package main
import(
"custom_package/person"
"fmt"
)
func main(){
p := person.Description("Milap")
fmt.Println(p)
}
// => The person name is: Milap
現在,就可以引入包,并調用
Description
方法了。注意,
secretName
方法是小寫字母開頭的私有方法,是以不能被外部調用。
包的文檔
Go内置了對封包檔的支援。運作以下指令生成文檔:
go doc person Description
這将為
person
包生成
Description
函數的文檔。請使用以下指令運作Web伺服器,檢視文檔:
godoc -http=":8080"
打開這個連結http://localhost:8080/pkg/,就能看到文檔了。
Go中的一些内置包
fmt
fmt
包實作了格式化I/O功能。我們已經使用過這個包列印内容到标準輸出流了。
json
另外一個很有用的包是
json
,用來編碼/解碼
Json
資料。
// 編碼
package main
import (
"fmt"
"encoding/json"
)
func main(){
mapA := map[string]int{"apple": 5, "lettuce": 7}
mapB, _ := json.Marshal(mapA)
fmt.Println(string(mapB))
}
// 解碼
package main
import (
"fmt"
"encoding/json"
)
type response struct {
PageNumber int `json:"page"`
Fruits []string `json:"fruits"`
}
func main(){
str := `{"page": 1, "fruits": ["apple", "peach"]}`
res := response{}
json.Unmarshal([]byte(str), &res)
fmt.Println(res.PageNumber)
}
//=> 1
使用
Unmarshal
解碼json位元組時,第一個參數是json位元組,第二個是期望解碼後的結構體指針。注意:
json:"page"
負責把
page
映射到結構體中的
PageNumber
字段上。
錯誤處理
報錯是程式中的意外産物。假如我們正在使用
API
調用一個外部服務。這個
API
調用可能成功,也可能失敗。比如,可以使用以下方法,處理報錯:
package main
import (
"fmt"
"net/http"
)
func main(){
resp, err := http.Get("http://example.com/")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp)
}
傳回自定義錯誤
在寫函數時,我們可能會遇到需要報錯的情景,這時可以傳回一個自定義的
error
對象。
func Increment(n int) (int, error) {
if n < 0 {
// return error object
return nil, errors.New("math: cannot process negative number")
}
return (n + 1), nil
}
func main() {
num := 5
if inc, err := Increment(num); err != nil {
fmt.Printf("Failed Number: %v, error message: %v", num, err)
}else {
fmt.Printf("Incremented Number: %v", inc)
}
}
大部分的内置包或者外部包,都有自己的報錯處理機制。是以我們使用的任何函數可能報錯,這些報錯都不應該被忽略,應該像上面示例中,在調用函數的地方,優雅地處理報錯。
Panic
當程式在運作過程中,突然遇到了未處理的報錯,就會導緻
panic
。在Go中,更推薦使用
error
對象,而不是
panic
來處理異常。發生
panic
後,程式會停止運作,但會運作
defer
語句代碼。
//Go
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
Defer
Defer
語句總是在函數最後執行。
在上面的栗子中,我們觸發了
panic
,但是
defer
語句依然會在最後執行。
Defer
适用于 需要在函數最後執行某些操作的場景,比如關閉檔案。
并發
Go在設計時考慮了并發性。 Go中的并發可以通過輕量級線程
Go routines
來實作。
Go routine
Go routine
是一個函數,它可以與另一個函數并行或并發執行。 建立
Go routine
非常簡單,隻需在函數前面添加關鍵字
go
,就可以使其并行執行。 同時,它很輕量級,是以可以建立上千個
routine
。
package main
import (
"fmt"
"time"
)
func main() {
go c()
fmt.Println("I am main")
time.Sleep(time.Second * 2)
}
func c() {
time.Sleep(time.Second * 2)
fmt.Println("I am concurrent")
}
//=> I am main
//=> I am concurrent
上面的示例中,c函數是一個
Go routine
,與main函數中的線程并行。有時我們想在多個線程之間共享資源。 Go傾向于不與另一個線程共享變量,因為這會增加死鎖和資源等待的可能。但是仙人自有妙招,就是接下來講到的
go channel
。
Channels
我們可以使用
channel
在兩個
routine
之間傳遞資料。建立
channel
時,需要指定其接收的資料類型。
c := make(chan string)
通過上面建立的
channel
,我們可以發送/接收
string
類型的資料。
package main
import "fmt"
func main(){
c := make(chan string)
go func(){ c <- "hello" }()
msg := <-c
fmt.Println(msg)
}
//=>"hello"
接收方
channel
會一直等待發送方發資料到
channel
。
單向channel
在某些場景下,我們希望
Go routine
隻接收資料但不發送資料,反之亦然。 這時,我們可以建立一個單向
channel
。
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
go sc(ch)
fmt.Println(<-ch)
}
// sc函數:隻能發送資料給 channel,不能接收資料
func sc(ch chan<- string) {
ch <- "hello"
}
使用 select
語句在 Go routine
中處理多個 channel
select
Go routine
channel
一個函數可能正在等待多個通道。這時,我們可以使用
select
語句。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go speed1(c1)
go speed2(c2)
fmt.Println("The first to arrive is:")
select {
case s1 := <-c1:
fmt.Println(s1)
case s2 := <-c2:
fmt.Println(s2)
}
}
func speed1(ch chan string) {
time.Sleep(2 * time.Second)
ch <- "speed 1"
}
func speed2(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "speed 2"
}
// => The first to arrive is:
// => speed 2
Buffered channel
在Go中,你還可以使用緩沖區
channel
,如果緩沖區已滿,發送到該
channel
的消息将被阻塞。
package main
import "fmt"
func main(){
ch := make(chan string, 2)
ch <- "hello"
ch <- "world"
ch <- "!" // extra message in buffer
fmt.Println(<-ch)
}
// => fatal error: all goroutines are asleep - deadlock!
最後唠唠嗑
為什麼
Golang
能夠成功呢?
Simplicity… — Rob-pike
因為簡單...
好了,本文終于結束了!你從菜鳥變成大佬了嗎?開個玩笑,希望看完能有所收獲。
歡迎關注:測試開發Guide,持續更新測試開發幹貨、面試資料。作者: wwwn
出處:部落格園
原文連結: https://www. cnblogs.com/wwwn/p/1280 4506.html