Go 工程師,月薪一萬起

7 天從 Java 工程師轉型爲 Go 工程師

爲什麼要捨棄 Java,投奔 Go?

我從 2014 年開始接觸 Java,對 Java 這門語言有着一種母語情結。相比於 C,Java 擁有面向對象、支持跨平臺、垃圾回收等優秀特性。且 Java 的強大而成熟的生態體系、豐富的工具類等使 Java 能夠支持了無數大型的系統架構。所以被國內以阿里爲代表的廣大企業用作編程主語言。

那麼既然 Java 這麼厲害,爲什麼還需要學 go 呢?

我的回答是 :Go 比 Java 更加簡單、好用。

我們來簡單地看一段 Java 和 go 的代碼,就拿實現併發來說

顯而易見,在代碼簡潔度上 Go 可以說秒殺 Java。

當然,Go 語言除了簡單、好用以外,也有其他的一些非常強大的特性。

包括:

 1. 併發與協程
 2. 基於消息傳遞的通信方式
 3. 豐富實用的內置數據類型
 4. 函數多返回值
 5. defer 機制
 6. 反射(reflect)
 7. 高性能 HTTP Server

因爲 Go 語言的這些優勢,所以最近幾年得到了很大的發展。很多新興技術包括 docker、kubernetes 等雲端技術以及區塊鏈等都是使用 Go 語言進行開發的。從這個意義上來說,Go 語言是比較“潮”的語言。技多不壓身,那麼就讓我們開啓 Go 語言之旅吧。

Go quick start

環境安裝

安裝環境比較簡單,這裏就不贅述了,具體可參考:http://www.runoob.com/go/go-environment.html

GoPath 和 GoRoot

通過上面教程的環境安裝,你可以順利輸出一個 hello world,那麼這個過程中發生了什麼呢?先來看代碼

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

使用過 Java 的同學應該是對 package 很熟悉了,Go 裏面 package 的概念和 Java 是類似的。要生成 Go 可執行程序,必須建立一個名爲 main 的package,並且在該 package 中必須包含一個名爲 main() 的函數。“fmt” 也是一個包,那麼這個包是怎麼找到的呢?這裏就要引出 GoRoot 和 GoPath 了,GoPath 是 Go 的工作目錄,GoRoot 是 Go 的安裝目錄。

使用 go env 命令可以查看 GoPath 和 GoRoot: GoPath 目錄約定有三個子目錄

  • src:存放源代碼。按照 Go 語言約定,go run,go install 等命令默認會在此路徑下執行;
  • pkg:存放編譯時生成的中間文件( * .a );
  • bin: 存放編譯後生成的可執行文件 ( 在項目內執行 go install,會在 bin 目錄下生成一個可執行文件)。

當我們要引用 GitHub 上的開源包時,比如使用 https://github.com/garyburd/redigo 這個包來用 golang 進行 redis 的操作。可以執行 go get https://github.com/garyburd/redigo 命令,將會在 GoPath 的 src 目錄下生成一個 /github.com/garyburd/redigo 這樣的目錄結構。

即可在項目中像引用 fmt 包一樣引用

        package main import ( "fmt" "github.com/garyburd/redigo/redis" ) func main() {
            c, err := redis.Dial("tcp", "127.0.0.1:6379")
            if err != nil {
                fmt.Println("Connect to redis error", err)
                return
            }
            fmt.Println("redis connect succ")
            defer c.Close()
        }

編譯和運行

我們先把隨便找一個空目錄,新建 test.go,貼上代碼啓動 redis ,執行 go run test.go,即可出現 “redis connect succ” , 程序執行成功。

Go 命令

Go 提供了很多命令,包括打包、格式化代碼、文檔生成、下載第三方包等等諸多功能,我們在命令行工具上執行 go --help ,如下:

這裏重點介紹幾個常用的命令

  • 編譯並執行,只能作用於命令源碼文件,一般用於開發中快速測試。上文我們通過執行 go run test.go ,完成了程序的編譯運行。

  • go build 編譯代碼包或者源碼文件。如果帶上代碼包名,則表示編譯指定的代碼包;如果帶上源碼文件,則表示編譯指定源碼文件。

  • go get 下載第三方代碼包並編譯安裝 ,需要注意的是,它會下載安裝到 GOPATH 環境變量配置的第一個工作區中。上文我們在使用 Go 連接 redis 時,通過 go get https://github.com/garyburd/redigo 命令,將 redis 包下載到 GoPath 的 src 目錄下並進行編譯安裝。然後在代碼中進行 import “github.com/garyburd/redigo/redis” 這個包則完成了導入。

  • go install 這個命令用於編譯安裝,可以作用於 main 包和非 main 包,然後將編譯後的生成的執行文件存放到工程的 bin 目錄下,將生成的歸檔文件(即靜態鏈接庫)存放到工程的 pkg 目錄下。使用方式類似於 go build,可以在某個代碼包目錄下直接使用,也可以指定代碼包使用。

    比如在我們剛剛執行 go get https://github.com/garyburd/redigo 進行了 redis 包的編譯安裝。假如我們不使用 go get,而是通過下載 zip 壓縮包的形式,在 GoPath 的 src 目錄下建立解壓,形成 github.com/garyburd/redigo 的目錄結構 ,然後執行 go install github.com/garyburd/redigo/redis ,我們會發現在 GoPath 的 pkg 目錄下會生成一個 這樣的目錄 /darwin_amd64/github.com/garyburd/redigo,redigo 目錄下會生成一個 redis.a 的文件。

    .a文件是編譯過程中生成的,每個package都會生成對應的.a文件,Go在編譯的時候先判斷package的源碼是否有改動,如果沒有的話,就不再重新編譯.a文件,這樣可以加快速度。

    • go env 用於打印GO語言的環境信息,如 GOPATH 是工作區目錄,GOROOT 是 GO 語言安裝目錄,GOBIN 是通過 go install 命令生成可執行文件的存放目錄(默認是當前工作區的 bin 目錄下),GOEXE 爲生成可執行文件的後綴
  • go vet 代碼靜態檢查工具,如 go vet test.go。

完成 Java 語法 到 Go 語法的迅速切換

申明與賦值

數據類型

和 Java 類似,Go 也有基本數據類型和引用數據類型。

基本數據類型

  • 布爾型:Java 申明用 boolean, Go 用 bool。
  • 整型:Go 裏面有 int8 int16 int32 uint64 ,分別對應 Java 中的 byte short int long,同時 Go 裏面還有專門 表示無符號數的 uint8 uint 16 uint32 uint64。
  • 浮點型:Go 裏面 有 float32 float64 分別對應 Java 的 float 和 double。
  • 字符串類型:Go 和 Java 一樣,都是 string。
  • 派生類型:包括 指針類型(Pointer)、數組類型、結構化類型(struct)、Channel 類型、函數類型、切片類型、接口類型(interface)、Map 類型 等。
變量申明賦值

Go 語言變量申明賦值有三種方式

先聲明、後賦值

例如 :

    var name string
    name = "diuge"

Go 語言中用 var 申明變量,Java 語言中是以 分號 “ ; ” 進行語句分割,所以行末會有分號,而 Go 中沒有.

Java 中變量申明是類型在變量前面,例如 string name , 但 Go 中相反,是變量在類型前,例如上文 var name string。

申明並且賦值

var name  = "diuge"

編譯器會根據值自行判定變量類型

使用 " := " 申明並賦值

name := "diuge"

這裏省略 var 關鍵字

條件、循環語句

條件語句

Go 語言中 if 語句的語法如下:

            if 布爾表達式 {
               /* 在布爾表達式爲 true 時執行 */
            }

需要注意的是,Go 中條件語句,if 沒有括號,例如

        if a < 20 {
           /* 如果條件爲 true 則執行以下語句 */
           fmt.Printf("a 小於 20\\n" )
        }

此外,if 還有另外一種形式,它包含一個 statement 可選語句部分,該組件在條件判斷之前運行。它的語法是

            if statement; condition {  

            }

例如:

        if a := 10  ;  a < 20 {
               /* 如果條件爲 true 則執行以下語句 */
               fmt.Printf("a 小於 20\\n" )
            }

循環語句

與 Java 不同的是,Go 語言只提供了 for 循環,沒有 while 循環。

但是在 Go 中,可以使用 for 循環實現 Java while 循環的效果。

Go 語言的 For 循環有 3 種形式,只有其中的一種使用分號。

第一種 :和 Java 的 for 語法類似,語法格式如下 :

        for init; condition; post { 

        }
        init: 一般爲賦值表達式,給控制變量賦初值;
        condition: 關係表達式或邏輯表達式,循環控制條件;
        post: 一般爲賦值表達式,給控制變量增量或減量。

        例如 : for i :=0 ; i < 10 ; i ++ {
                    fmt.Println("i = " + i)
               }

這裏是不是很熟悉? 跟 Java 唯一不一樣的地方在於 Go 中 for 循環沒有小括號。

第二種 :和 Java while 循環 類似,語法格式如下 :

        for condition { }
        例如 : 
                for 0 == 0 {}
                for true {}

第三種 : 死循環,類似 while (true) {} , 語法格式如下:

            for { }

異常處理

學 Java 的大家可能都知道,Java 中有 exception 的 try — catch — finally 異常捕獲機制 ,Go 語言中一般不採用這種異常捕獲機制,而是通過下面幾種方式進行異常處理。

error 接口

Go 語言通過內置的錯誤接口提供了非常簡單的錯誤處理機制。

error 類型是一個接口類型,這是它的定義:

        type error interface {
            Error() string
        }

使用 errors.New () 可以返回一個 error 信息 例如 :

        func checkParam(username string, password string) error {
                if username == "" || password == "" {
                        return errors.New("params error")
                }
        }

一般來說, error 是 Go 語言中最常見的處理錯誤的方式,通過返回 error,處理 error, 產生類似 Java exception 的效果。

defer 語句

在 Go 語言中,可以使用關鍵字defer向函數註冊退出調用,即主調函數退出時,defer後的函數纔會被調用

defer 語句的作用是不管程序是否出現異常,均在函數退出時自動執行相關代碼。(相當於 Java 中的 finally

例如 :

            func main() {
                for i := 0; i < 5; i++ {
                    defer fmt.Println(i)
                }
            }

        其執行結果爲 : 
            4
            3
            2
            1
            0

所以,我們在進行數據庫連接、文件、鎖 操作時 ,一般都會使用 defer 語句進行數據庫連接的釋放,釋放文件句柄和鎖的釋放。

panic-recover 機制

學過 Java 的同學都知道,Java 中異常分爲運行時異常和非運行時異常。我們在上文說到可以在方法中拋出 error 錯誤來實現異常的捕獲, error 只能針對預期內的錯誤,因爲你是預判這段程序可能出現 異常邏輯,纔會去主動調用 errors.New () 生成一個 error 。但是對於一個方法來說,我們不可能預判到所有的異常情況,那假如某一個隱藏 bug 導致程序崩潰了怎麼辦呢?這裏就需要引入 panic-recover 機制了。

假如代碼運行時異常崩潰了,此時 Go 會自動 panic,Go 的每次 panic 都是非常消耗性能的,且 Go 是單線程,所以,我們應該儘量去避免 使用 panic。

panic () 是一個內建函數,可以中斷原有的控制流程,進入一個令人 panic (恐慌 ,即 Java 中的異常)的流程中。當函數 F 調用 panic,函數 F的執行被中斷,但是 F 中的延遲函數(必須是在 panic 之前的已加載的 defer )會正常執行,然後 F 返回到調用它的地方。在調用的地方,F 的行爲就像調用了panic。這一過程繼續向上,直到發生 panic 的 goroutine 中所有調用的函數返回,此時程序退出。異常可以直接調用 panic 產生。也可以由運行時錯誤產生,例如訪問越界的數組。

recover () 是一個內建的函數,可以讓進入令人恐慌的流程中的 goroutine 恢復過來。recover 僅在延遲函數中有效。在正常的執行過程中,調用 recover 會返回 nil ,並且沒有其它任何效果。如果當前 goroutine 陷入 panic ,調用 recover 可以捕獲到 panic 的輸入值,並且恢復正常的執行。

一般情況下,recover () 應該在一個使用 defer 關鍵字的函數中執行以有效截取錯誤處理流程。如果沒有在發生異常的 goroutine 中明確調用恢復過程(使用 recover 關鍵字),會導致該 goroutine 所屬的進程打印異常信息後直接退出。

這裏結合自定義的 error 類型給出一個使用 panic 和 recover 的完整例子:

    package main import ( "fmt" ) //定義除法運算函數 func Devide(num1, num2 int) int {
        if num2 == 0 {
            panic("num cannot be 0") 
        } else {
            return num1 / num2
        }
    }
    func main() {
        var a, b int
        fmt.Scanf("%d %d", &a, &b)

        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("panic的內容%v\\n", r)
            }
        }()

        rs := Devide(a, b)
        fmt.Println("結果是:", rs)
    }

Go 語言特性

這裏指的特性是數據類型以及語法上的特性,不上升到綜合層次

函數

在 Java 中,函數是隻能有單一返回值的,但是在 Go 中,函數可以支持多返回值。

例如 :

        func swap ( x string , y string ) ( string , string ){
             return y , x
        }

Go 指針

Java 是不支持指針類型的,但是學過 C 的同學可能對指針有或多或少的瞭解,Go 語言也是支持指針類型的。

我們都知道,變量是一種使用方便的佔位符,用於引用計算機內存地址。Go 語言的取地址符是 &,放到一個變量前使用就會返回相應變量的內存地址。

        package main
        import "fmt"

        func main() {
           var a int = 10   
           fmt.Printf("變量的地址: %x\\n", &a  )
        }

上面程序的輸出結果爲:

    變量的地址: 20818a220

指針聲明格式如下:

        var var_name *var-type

var-type 爲指針類型,var_name 爲指針變量名,* 號用於指定變量是作爲一個指針。以下是有效的指針聲明:

        var ip *int        /\* 指向整型\*/
        var fp *float32    /\* 指向浮點型 \*/

在指針類型前面加上 * 號(前綴)即可獲取指針所指向的內容。

        package main

        import "fmt"

        func main() {
           var a int= 20   /\* 聲明實際變量 \*/
           var ip *int        /\* 聲明指針變量 \*/

           ip = &a  /\* 指針變量的存儲地址 \*/

           fmt.Printf("a 變量的地址是: %x\\n", &a  )

           /\* 指針變量的存儲地址 \*/
           fmt.Printf("ip 變量儲存的指針地址: %x\\n", ip )

           /\* 使用指針訪問值 \*/
           fmt.Printf("\*ip 變量的值: %d\\n", *ip )
        }

指針的操作比較複雜,這裏也只是對指針進行了簡單的介紹,定義和程序都來自第三方網站,所以大家想更多瞭解指針的話可以自行研究一下,這裏不贅述。

**結構體 **

Go 語言中結構體類似 Java 中的類 ( class )**

例如, Java 中對一個 Person 類的定義如下 :

        class Person {
             string name;
             int age;
        }

在 Go 語言中, 用結構體 ( struct ) 表示如下 :

        type Person struct {
              name string
              age int
        } 

**切片 **

Java 中的數組長度在申明時就已經固定了,但是 Go 提供了一種類似 “ 動態數組 ” 結構的數據類型,這種類型就是切片 slice。

slice 的本質是一個數據結構,實現了對數組操作的封裝。

切片 slice 的申明語法如下 :

            var identifier []type

你可以申明一個 int32 類型的 slice

            var array []int32

你會發現 slice 和數組的申明語法是不同的。

Go 語言中申明數組時,是需要指定長度的, 比如 :

            var array [10] int32

初始化操作也不一樣,對 slice 的初始化是使用 make 初始化

            array = make ( []int32 , 10 )

而對數組的初始化是

    array = [10] int32 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

對 slice 的操作是 Go 語言中最常用的操作之一,包括 :

  • append : 實現對 slice 元素的添加 例如 :
            array := make ( []int32 , 10 )
            array = append ( array, 1 )
            array = append ( array, 2 )

  • 截取 : 可以通過設置下限及上限來設置截取切片 [lower-bound:upper-bound],實例如下:
        package main

        import "fmt"

        func main() {
           /\* 創建切片 \*/
           numbers := []int{0,1,2,3,4,5,6,7,8}   

           /\* 打印原始切片 \*/
           fmt.Println("numbers ==", numbers)

           /\* 打印子切片從索引1(包含) 到索引4(不包含)\*/
           fmt.Println("numbers[1:4] ==", numbers[1:4])

           /\* 默認下限爲 0\*/
           fmt.Println("numbers[:3] ==", numbers[:3])

           /\* 默認上限爲 len(s)\*/
           fmt.Println("numbers[4:] ==", numbers[4:])

        }

        func printSlice(x []int){
           fmt.Printf("len=%d slice=%v \\n" , len(x) , x)
        }

     執行以上代碼輸出結果爲:

        numbers == [0 1 2 3 4 5 6 7 8]
        numbers[1:4] == [1 2 3]
        numbers[:3] == [0 1 2]
        numbers[4:] == [4 5 6 7 8]

  • 獲取切片長度 : 通過 len 方法,可以獲取 slice 長度
        len ( slice )

  • 遍歷 : 通過 range 關鍵字進行遍歷,range 遍歷是 Go 中特有的一種遍歷方式,除了可以對 slice 進行遍歷,還可以對 數組、map、string 字符串 等進行遍歷。例如 :
        package main
        import "fmt"
        func main() {
            //這是我們使用range去求一個slice的和。使用數組跟這個很類似
            nums := []int{2, 3, 4}
            sum := 0
            for _, num := range nums {
                sum += num
            }
            fmt.Println("sum:", sum)
            //在數組上使用range將傳入index和值兩個變量。上面那個例子我們不需要使用該元素的序號,所以我們使用空白符"\_"省略了。有時侯我們確實需要知道它的索引。
            for i, num := range nums {
                if num == 3 {
                    fmt.Println("index:", i)
                }
            }
            //range也可以用在map的鍵值對上。
            kvs := map[string]string{"a": "apple", "b": "banana"}
            for k, v := range kvs {
                fmt.Printf("%s -\> %s\\n", k, v)
            }
            //range也可以用來枚舉Unicode字符串。第一個參數是字符的索引,第二個是字符(Unicode的值)本身。
            for i, c := range "go" {
                fmt.Println(i, c)
            }
        }

**併發 **

之前說到了,相比於 Java ,Go 是天然支持併發的

在 go 語言中,每一個線程我們把它叫做 goroutine

goroutine 是輕量級線程,goroutine 的調度是由 Golang 運行時進行管理的。

goroutine 語法格式:

go 函數名( 參數列表 ),例如:

            go f (x, y, z)

表示新開一個線程 執行 f (x , y , z) 這個方法。

strong text 更多併發的知識點下文會繼續介紹,這裏不再贅述。

Java 面向對象特性在 Go 中是如何實現的

我們知道 Java 這門語言的流行跟 它面向對象的特性是分不開的。那麼 Java 的封裝、繼承、多態 在 Go 中是如何實現的呢?

封裝

Java 中的封裝主要是通過訪問權限控制實現的。

在 Go 語言中,並沒有 public ,private 這些權限控制符。那麼 go 是如何實現 結構體的封裝的呢 ?

在 Go 語言中,是通過約定來實現權限控制的。變量和方法都遵守駝峯式命名。變量和方法的首字母大寫,相當於 public,變量和方法的首字母小寫,相當於 private。同一個包中訪問,相當於 default ,由於 Go 語言沒有繼承,所以也沒有 protected 權限。

上面剛說到,Go 語言是沒有繼承的。但是 Go 語言可以通過結構體之間的組合來實現類似 Java 中繼承的效果。

假如把 Go 中 struct 看做 Java 中的類,在 struct 中可以包含其他的struct,繼承內部 struct 的方法和變量,同時可以重寫,代碼如下:

        package main

        import "fmt"

        type oo struct {
            inner
            ss1 string
            ss2 int
            ss3 bool
        }

        type inner struct {
            ss4 string
        }

        func (i *inner) testMethod () {
            fmt.Println("testMethod is called!!!")
        }

        func main() {
            oo1 := new(oo)
            fmt.Println("ss4無值:"+oo1.ss4)
            oo1.ss4 = "abc"
            fmt.Println("ss4已賦值"+oo1.ss4)
            oo1.testMethod()//繼承調用
            oo1.inner.testMethod()//繼承調用 這裏也可以重寫
        }

多態

Java 中的多態是通過 extends class 或者 implements interface 實現的,在 Go 中既沒有 extends,也沒有 implements ,那麼 Go 中是如何實現多態的呢 ?

我們來看以下代碼,Girl 和 Boy 都實現了 Person 。 在 Go 語言中,只要某個 struct 實現了某個 interface 的所有方法,那麼我們就認爲這個 struct 實現了這個類(相當於 Java 中的implements)。

            package main import ( "fmt" ) type Person interface {
                Sing ()
            }

            type Girl struct {
                Name string
            }

            type Boy struct {
                Name string
            }

            func (this *Girl) Sing () {
                fmt.Println("Hi, I am " + this.Name)
            }

            func (this *Boy) Sing () {
                fmt.Println("Hi, I am " + this.Name)
            }

            func main() {
                g := &Girl{"Lucy"}
                b := &Boy{"Dave"}

                p := map[int]Person{}
                p[0] = g
                p[1] = b

                for _, v := range p {
                    v.Sing()
                } 
            }

Go 併發

Java 中的併發是通過繼承 Thread 類或者實現 Runnable 接口來實現的。我們前面說過,Go 併發通過 Go 關鍵字就可以實現,go f (x, y, z) 的形式即可以通過 新開 goroutine 實現 Go 併發。那麼 goroutine 之間是如何進行通信與同步的呢

channel

channel 是 goroutine 之間通信的一種方式,可以類比成 Unix 中的進程的通信方式管道。

可以用 channel 操作符 <- 對其發送或者接收值。

        ch <- v    // 將 v 送入 channel ch。
        v := <-ch  // 從 ch 接收,並且賦值給 v。
        (“箭頭”就是數據流的方向。)

和 map 與 slice 一樣,channel 使用前必須創建:

        ch := make(chan int)

默認情況下,在另一端準備好之前,發送和接收都會阻塞。這使得 goroutine 可以在沒有明確的鎖或競態變量的情況下進行同步。

例如 :

        package main

        import "fmt"

        func sum(a []int, c chan int) {
            sum := 0
            for _, v := range a {
                sum += v
            }
            c <- sum // 將和送入 c
        }

        func main() {
            a := []int{7, 2, 8, -9, 4, 0}

            c := make(chan int)
            go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // 從 c 中獲取

            fmt.Println(x, y, x+y)
        }

channel 可以是帶緩衝的。爲 make 提供第二個參數作爲緩衝長度來初始化一個緩衝 channel:

        ch := make(chan int, 100)

向緩衝 channel 發送數據的時候,只有在緩衝區滿的時候纔會阻塞。當緩衝區清空的時候接受阻塞。

同時,可以使用 close© 來關閉一個 channel。

select

select 語句使得一個 goroutine 在多個通訊操作上等待。

select 會阻塞,直到條件分支中的某個可以繼續執行,這時就會執行那個條件分支。當多個都準備好的時候,會隨機選擇一個。

當 select 中的其他條件分支都沒有準備好的時候,default 分支會被執行。

例如 :

        package main

        import (
            "fmt"
            "time"
        )

        func main() {
            tick := time.Tick(100 * time.Millisecond)
            boom := time.After(500 * time.Millisecond)
            for {
                select {
                case <-tick:
                    fmt.Println("tick.")
                case <-boom:
                    fmt.Println("BOOM!")
                    return
                default:
                    fmt.Println(" .")
                    time.Sleep(50 * time.Millisecond)
                }
            }
        }

這裏只介紹一種互斥鎖,其他類型的鎖讀者可以自行了解。

互斥鎖用 var mutex sync.Mutex 申明 ,通過 mutex.Lock() 進行加鎖,通過 mutex.Unlock() 進行解鎖。

例如 :

        func main() {
            var common int = 100
            var mutex sync.Mutex
            for {
                go func() {
                    mutex.Lock()
                    if common > 0 {
                        common --
                        fmt.Println(common)
                    }
                    mutex.Unlock()
                }()
            }
        }

這裏 common 是對所有線程都可見的共享內存,所以對common 的操作需要加鎖。我們發現 Go 語言的互斥鎖的使用是非常簡單的,這也符合 Go 語言的設計理念。

Go RPC 調用

RPC 最常用的方式還是走 HTTP 協議,可以有 GET 和 POST 兩種方式。

GET

這裏貼上一個簡單的發送 HTTP Get 請求的代碼:

            package main

            import (
                "fmt"
                "io"
                "net/http"
                "os"
            )

            func main() {
                // 生成默認client
                client := &http.Client{}

                // 生成要訪問的url
                url := "http://www.baidu.com"

                // 構造請求
                request, err := http.NewRequest("GET", url, nil)

                if err != nil {
                    fmt.Println("new request error : " + err.Error())
                }

                // 處理返回結果
                response, err := client.Do(request)

                // 處理錯誤
                if err != nil {
                    fmt.Println("do request error : " + err.Error ())
                }

                body, err := ioutil.ReadAll ( resp.Body )

                fmt.Println( string (body) )

            }

POST

比較簡單,直接貼代碼 :

            func request () {

                client := &http.Client{}

                req, err := http.NewRequest("POST", "www.baidu.com", strings.NewReader(""))

                if err != nil {
                    // handle error
                }

                req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
                req.Header.Set("Cookie", "uin=1812672342")

                resp, err := client.Do(req)

                defer resp.Body.Close()

                body, err := ioutil.ReadAll(resp.Body)
                if err != nil {
                    // handle error
                }

                fmt.Println(string(body))
            }

一個小 Demo:用 Go 語言實現抽獎系統

附送上一個小 demo ,使用 Go 寫的,非常簡單,本來獎品應該用 redis 存儲,這裏偷懶了一下,通過配置文件初始化了。

講一下 抽獎算法是參考 微信平臺的抽獎算法:https://www.xuanfengge.com/luck-draw.html(可以防止獎品被很快抽完,同時保證每個人的抽獎概率是相同的)。這裏用 Go 重寫了抽獎算法,如下:

        func GetAward(awardBatches []AwardBatch) (AwardBatch, error) {

            startTime , _ := ParseStringToTime(conf.Award.StartTime)
            endTime , _ := ParseStringToTime(conf.Award.EndTime)

            award , err := RandomGetAwardBatch(awardBatches)
            if err != nil {
                return AwardBatch{}, err
            }

            totalAmount := award.GetTotalAmount()
            totalBalance := award.GetTotalBalance()
            updateTime := award.GetUpdateTime()

            detaTime := (endTime - startTime) / totalAmount
            currentTime := time.Now().Unix()

            r := rand.New(rand.NewSource(updateTime))
            // 計算下一個獎品的釋放時間
            releaseTime := startTime + (totalAmount - totalBalance) * detaTime +  int64(r.Int()) % detaTime

            fmt.Println("releaseTime : " + fmt.Sprintf("%d", releaseTime) + " currentTime : " + fmt.Sprintf("%d",currentTime))

            if (currentTime < releaseTime) {
                return AwardBatch{} , errors.New(" currentTime not in award release period ")
            }

            return award, nil
        }

        func RandomGetAwardBatch(awardBatches []AwardBatch) ( AwardBatch , error ) {

            if len(awardBatches) == 0 {
                return AwardBatch{} , errors.New("empty param awardBatches")
            }

            weight := int64(0)

            for _, awardBatch := range awardBatches {
                weight += awardBatch.GetTotalBalance()
            }

            if weight == 0 {
                return AwardBatch{}, errors.New("weight is 0")
            }

            r := rand.New(rand.NewSource(weight))

            num := r.Int63n(weight)

            for _, awardBatch := range awardBatches {
                num -= awardBatch.GetTotalBalance()

                if num < 0 {
                    return awardBatch , nil
                }
            }

        return AwardBatch{}, errors.New("randomGetAwardBatch should shoot at least one batch")

        }

我把它放在了 GitHub 上 https://github.com/diubrother/award,可以用來當作 Go 入門級的一個小 demo。

我的 Go 入門之路

爲什麼說 7 天轉型呢?因爲我當時就是用了 7 天左右。寫 Go 的第一行代碼時,很不適應,無論是變量的命名,還是沒有行末分隔符,以及 Go 最讓我不適應的一點是 數據類型只能顯示轉換,舉個例子,如果不顯示把 uint 32 轉成 int64 或者 uint64 ,你的程序是永遠編譯不通過的。

個人覺得,學一門語言最快還是從使用開始,語法看個一兩天,一定要對着用例 code ,對語法有個基本的印象就可以開始寫需求或者功能了。可以嘗試着寫一些小的需求。我當時就是通過一個小的業務需求,直接對着別人的代碼抄,然後抄個兩三天,把你的代碼編譯、調試、測試、上線等整套流程走完,基本上就算是初步上手了。

這裏推薦一個我當時入門看的兩個網站,這篇 chat 裏面幾個例子也是引用了這兩個網站的。

第一個是大家熟知的菜鳥教程 : http://www.runoob.com/go/go-tutorial.html (1-2 day 熟悉一下 go 語法)

第二個是 go 指南 :http://tour.studygolang.com/welcome/1 (這個相對於菜鳥教程的優勢就是有比較多的例子)

好了,言盡於此,祝君好運。

歡迎關注我的公衆號,回覆關鍵字“大禮包” ,將會有大禮相送!!! 祝各位面試成功!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章