Golang快速入門:從菜鳥變大佬

最近寫了很多Go代碼,可是寫着寫着,仍是容易忘,尤爲是再寫點Python代碼後。因此找了一篇不錯的Golang基礎教程,翻譯一下,時常看看。git

原文連接: 「Learning Go — from zero to hero」 by Milap Neupanegithub

開始

Go是由各類 包 組成的。main包是程序的入口,由它告訴編譯器,這是一個可執行程序,而不是共享包。main包定義以下:golang

package main

工做區

Go的工做區是由環境變量GOPATH決定的。
你能夠在工做區裏爲所欲爲地寫代碼,Go會在GOPATH或者GOROOT目錄下搜索包。注:GOROOT是Go的安裝路徑。json

設置GOPATH爲你想要的目錄:數組

# export 環境變量
export GOPATH=~/workspace
# 進入工做區目錄
cd ~/workspace

在工做區目錄裏建立mian.go文件。bash

package main

import (
 "fmt"
)

func main(){
  fmt.Println("Hello World!")
}

咱們使用import關鍵字來引入一個包。func main是執行代碼的入口,fmt是Go的內置包,主要用來格式化輸入/輸出。而Println是fnt中的一個打印函數。服務器

想要運行Go程序,有兩種方法。併發

方法一

你們都知道,Go是一門編譯型語言,因此在執行以前,咱們須要先編譯它。app

> go build main.go

這個命令會生成二進制可執行文件 main,而後咱們再運行它。ide

> ./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還支持複數類型數據類型,可使用complex64complex128進行聲明。

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語句。

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

由於簡單...

好了,本文終於結束了!你從菜鳥變成大佬了嗎?開個玩笑,但願看完能有所收穫。

相關文章
相關標籤/搜索