1. 程式人生 > >go基礎簡介

go基礎簡介

Go語言是一種靜態強型別、編譯型、併發型,並具有垃圾回收功能的程式語言。Go 的正式語法使用分號來結束語句,但大多數情況下是可以省略的(編譯器會自動新增);如果你在一行中寫多個語句,則需要用分號隔開不同的語句:

  • Go程式是通過package來組織的;

  • 只有package名稱為main的包可以包含main函式;

  • 一個可執行程式有且只有一個main包;

  • 如果匯入的包但是沒有用到型別或者函式則會報編譯錯誤;

  • package別名: import io "fmt" 這樣將fmt設定一個別名為io,呼叫時為io.Println("...");

在 main.main 函式執行之前所有程式碼都執行在同一個goroutine

,也就是程式的主系統執行緒中。因此,如果某個 init 函式內部用go關鍵字啟動了新的goroutine的話,新的goroutine只有在進入 main.main 函式之後才可能被執行到。

go的註釋與C++類似:

  • 單行註釋 // ...

  • 多行註釋 /* ... */

資料型別

go中定義了以下基本資料型別:

  • 整數型別:

    • 與系統架構有關的:int與uint(在32位系統下為32,64位系統下為64);

    • 固定長度的:int8/16/32/64、uint8/16/32/64;

  • 浮點型別:float32、float64

  • 布林型別:bool

  • 複數型別: complex64、complex128

  • 字元型別: byte(uint8的別名)、rune(int32的別名,標識Unicode)

  • 字串型別: string

    • 一個字串是一個不可改變的位元組序列,可以包含任意的資料,包括byte值0;

    • for range 語法對UTF8字串提供了特殊支援;

    • 對字串和 []rune 型別的相互轉換提供了支援:如utf8.RuneCountInString(str);

    • 字串其實是一個結構體,因此字串的賦值操作也就是reflect.StringHeader 結構體的複製過程,並不會涉及底層位元組陣列的複製。

高階型別:

  • 陣列:[N]T,[...]T

  • 切片:[]T

  • 字典:map[K]T

  • 通道型別:chan T

    • 向無快取通道寫入時,會阻塞等待讀取;

    • 關閉nil或已關閉通道會引發panic;

    • 向已關閉的通道發生資料會引發panic;

    • 從已關閉的通道讀取,總是返回0值和false(對於快取通道,先讀取快取的資料);

自定義資料型別(可使用type定義):

  • 別名:type BuffSize int;BuffSize為新的型別,底層型別與int相同,但是為不同的型別,互操作時要進行型別轉換(BuffSize(var));

  • 自定義型別: type Name struct { ... }

c2 := make(chan struct{}) // 空結構體

go func() {

fmt.Println("c2")

    c2 <- struct{}{} // struct{}部分是型別, {}表示對應的結構體值

}()

<-c2

interface介面:golang不支援完整的面向物件思想,它沒有繼承,多型完全依賴介面實現;通過介面模擬繼承(本質是組合)。

  • Interface定義一組方法,且不能包含任何變數;

  • 只要型別包含了一個介面中的所有方法,那麼這個型別就實現這個介面;

  • 如果一個型別含有了多個interface的方法,那麼就實現了多個介面;

  • 如果一個型別只含有了一個interface的部分方法,那就沒有實現這個介面。

type User struct {

 Name string

 Age int32

}

var user User

var user1 *User = &User{}

var user2 *User = new(User)

// 實現Stringer介面(用於Sprintf)

func (p *User) Name() String{

 return p.name

}

類似下面的介面,要注意防止遞迴迴圈:

type MyString string

func (m MyString) String() string {

    // return fmt.Sprintf("MyString=%s", m) // 錯誤:會無限遞迴

    return fmt.Sprintf("MyString=%s", string(m)) // 可以:注意轉換

}

陣列與切片

Go語言中陣列是值語義。一個數組變數即表示整個陣列,它並不是隱式的指向第一個元素的指標(比如C語言的陣列),而是一個完整的值。當一個數組變數被賦值或者被傳遞的時候,實際上會複製整個陣列

陣列的大小是其型別的一部分,不同長度或不同型別的資料組成的陣列都是不同的型別,型別 [10]int 和 [20]int 是不同的。

切片是可以動態增長和收縮的序列,切片通過對陣列進行封裝,為資料序列提供了更通用、強大而方便的介面。若某個函式將一個切片作為引數傳入,則它對該切片元素的修改對呼叫者而言同樣可見, 這可以理解為傳遞了底層陣列的指標。

切片的容量可通過內建函式 cap 獲得,它將給出該切片可取得的最大長度。

儘管 Append 可修改 slice 的元素,但切片自身(其執行時資料結構包含指標、長度和容量) 是通過值傳遞的。所以append時需要使用返回值重新賦值:

我們還可以定義一個空的陣列,長度為0的陣列在記憶體中並不佔用空間。

var d [0]int // 定義一個長度為0的陣列

var f = [...]int{} // 定義一個長度為0的陣列

控制語句

控制語句包括if、for、switch 與 select,其的左大括號一定緊跟在同一行(不能放在下一行)。

if 和 switch 像 for 一樣可接受可選的初始化語句;沒有圓括號,而其主體必須始終使用大括號括住。

if err := file.Chmod(0664); err != nil {

    log.Print(err)

} else { // else必須跟在右括號後面,不能在下一行

    ...

}

迴圈只有一個更通用的 for

// Like a C for

for init; condition; post { }

// Like a C while

for condition { }

// Like a C for(;;)

for { }

閉包對捕獲的外部變數並不是傳值方式訪問,而是以引用的方式訪問。這種行為可能會導致一些隱含的問題:

for i := 0; i < 3; i++ {

func(){ println(i) } ()

}

每個函式引用的都是同一個i迭代變數,在迴圈結束後這個變數的值為3,因此最終輸出的都是3。修復的思路是在每輪迭代中為每個函式生成獨有的變數。

for i := 0; i < 3; i++ {

i := i // 定義一個迴圈體內區域性變數i

func(){ println(i) } ()

}

// 通過函式傳入i

for i := 0; i < 3; i++ {

func(i int){ println(i) } (i)

}

Go 沒有逗號操作符,而 ++ 和 -- 為語句而非表示式。 因此,若你想要在 for 中使用多個變數,應採用平行賦值的方式 (因為它會拒絕 ++ 和 --) 

for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {

    a[i], a[j] = a[j], a[i]

}

for key, value := range oldMap {

    newMap[key] = value

}

Go 的 switch 比 C 的更通用。其表示式無需為常量或整數,case 語句會自上而下逐一進行求值直到匹配為止;switch 並不會自動下溯(相當於預設有一個隱藏的break),只有在case中明確使用fallthrough關鍵字,才會繼續執行緊跟的下一個case;也 可通過逗號分隔來列舉相同的處理條件

switch c {

    case ' ', '?', '&', '=', '#', '+', '%':

    return true

}

switch 也可用於判斷介面變數的動態型別。

var t interface{}

t = functionOfSomeType()

switch t := t.(type) {

default:

    fmt.Printf("unexpected type %T", t) // 輸出 t 是什麼型別

case bool:

    fmt.Printf("boolean %t\n", t) 

case int:

    fmt.Printf("integer %d\n", t) // t 是 int 型別

case *int:

    fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 型別

}

當 select 有多個分支可用時,會隨機選擇一個可用的管道分支(可用於模擬隨機數生成),如果沒有可用的管道分支則選擇 default 分支,否則會一直儲存阻塞狀態。

for i := 0; i < n; i++ {

    select { // 通道中隨機寫入0/1

    case c <- 0:

    case c <- 1:

    }

}

函式

Go 與眾不同的特性之一就是函式和方法可返回多個值。返回值或結果 “形參” 可被命名,就像傳入的形參一樣,可賦值修改。 命名後:就會被初始化為零值; 若該函式執行了一條不帶實參的 return 語句,則結果形參的當前值將被返回。

func ReadFull(r Reader, buf []byte) (n int, err error) {

    for len(buf) > 0 && err == nil {

        var nr int

        nr, err = r.Read(buf)

        n += nr

        buf = buf[nr:]

    } 

    return // 若沒有命名,則必須return n,err

}

從函式中返回一個區域性變數的地址完全沒有問題,這點與 C 不同。該區域性變數對應的資料 在函式返回後依然有效。

錯誤處理

Go語言中的錯誤是一種介面型別。介面資訊中包含了原始型別和原始的值。只有當介面的型別和原始的值都為空的時候,介面的值才對應 nil 。其實當介面中型別為空的時候,原始值必然也是空的;反之,當介面對應的原始值為空的時候,介面對應的原始型別並不一定為空的。在處理錯誤返回值的時候,沒有錯誤的返回值最好直接寫為 nil。

func returnsError() error {

    var p *MyError = nil

    if bad() {

        p = ErrBad

    } 

    return p // 總是返回non-nil error;應直接return nil;

}

恐慌使用panic丟擲,recover用於捕獲panic。recover 函式呼叫有著更嚴格的要求:我們必須在 defer 函式中直接呼叫 recover ;如果是在巢狀的 defer 函式中呼叫 recover 也將導致無法捕獲異常。

示例程式

go程式的基本框架示例

package main // 包名

import(    // 引入包

 "fmt"

 "time"

 "context"

 "os"

 "os/signal"    // 使用signal引用包內元素

 "syscall"

 "strconv"

)

var _ = strconv.Itoa    // 防止未使用包報錯

func TryClose(put chan<- int){

defer func() {

if err := recover(); err!=nil{ // 捕獲恐慌,只能在defer中直接捕獲

fmt.Println("Close fail: ", err)

}

}()

close(put)

}

func Producer(fact int, put chan<- int, ctx context.Context){

OutFor:

for i:=1 ; ; i++ {

select{

default: 

case <-ctx.Done():  // 是否完成

fmt.Println(ctx.Err())

break OutFor // 跳到迴圈外部,否則只是跳出select

}

put <- fact * i

time.Sleep(100*time.Millisecond)

}

TryClose(put)

}

func Consumer(get <-chan int){

for v := range get {

fmt.Println(v)

}

fmt.Println("Consumer over")

}

type ByteSize float64

const (

_ = iota // 通過賦予空白識別符號來忽略第一個值0

KB ByteSize = 1 << (10 * iota)

MB

GB // 1 << 10*3

)

const MAXSIZE ByteSize = 1024*8

const MINSIZE = 16

func (p ByteSize)String() string{

switch{

case p>=GB:

return fmt.Sprintf("%.2fGB", p/GB)

case p>=MB:

return fmt.Sprintf("%.2fMB", p/MB)

case p>=KB:

return fmt.Sprintf("%.2f", p/KB)

}

return fmt.Sprintf("%.2fB", p)

}

func VarType(info interface{})(result int){

defer func() {result = result+1}()

// if v, ok := info.(int); ok{

//  fmt.Println("int: ", v)

//  result = v

// }else if v, ok := info.(string); ok{

//  fmt.Println("string: ", v)

// }else {

//  fmt.Printf("%v\n", info)

// }

        // 直接通過switch-type判斷

switch v := info.(type){

case int: fmt.Println("int: ", v)

case string: fmt.Println("string: ", v)

default: fmt.Printf("Deufalt: %v\n", info)

}

return // 0,實際返回1,因defer在return之後執行

}

func ForTest(que []int){

for _, v := range que{

v := v // 若不重新定義,會全部輸出最後一個值

go func() {

fmt.Println(v)

}()

}

}

func main(){

ch := make(chan int, 10)

        // 1秒鐘後超時,若在此之前cancel,也會退出

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

go Producer(3, ch, ctx)

go Producer(5, ch, ctx)

go Consumer(ch)

 // 不同變數定義方式

var zero = 0

var one, two int

three, four := 3, 4

ksize := ByteSize(1024*1024*8)

fmt.Println(zero, one, two, three, four, ksize)

VarType(1)

VarType("test")

ForTest([]int{1, 2, 3})

// Ctrl+C to quit

fmt.Println("Press Ctrl+C to quit:")

sig := make(chan os.Signal, 1)

signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

fmt.Printf("quit (%v)\n", <-sig) // 等待訊號

cancel()

}