golang錯題集

RyuGou發表於2018-07-31

本文即Go語言的那些坑三。

不要對Go併發函式的執行時機做任何假設

請看下列的列子:

import (
	"fmt"
	"runtime"
	"time"
)

func main(){
	names := []string{"lily", "yoyo", "cersei", "rose", "annei"}
	for _, name := range names{
		go func(){
			fmt.Println(name)
		}()
	}
	runtime.GOMAXPROCS(1)
	runtime.Gosched()
}
複製程式碼

請問輸出什麼?

答案:

annei
annei
annei
annei
annei
複製程式碼

為什麼呢?是不是有點詫異? 輸出的都是“annei”,而“annei”又是“names”的最後一個元素,那麼也就是說程式列印出了最後一個元素的值,而name對於匿名函式來講又是一個外部的值。因此,我們可以做一個推斷:雖然每次迴圈都啟用了一個協程,但是這些協程都是引用了外部的變數,當協程建立完畢,再執行列印動作的時候,name的值已經不知道變為啥了,因為主函式協程也在跑,大家並行,但是在此由於names陣列長度太小,當協程建立完畢後,主函式迴圈早已結束,所以,列印出來的都是遍歷的names最後的那一個元素“annei”。 如何證實以上的推斷呢? 其實很簡單,每次迴圈結束後,停頓一段時間,等待協程列印當前的name便可。

import (
	"fmt"
	"runtime"
	"time"
)

func main(){
	names := []string{"lily", "yoyo", "cersei", "rose", "annei"}
	for _, name := range names{
		go func(){
			fmt.Println(name)
		}()
		time.Sleep(time.Second)
	}
	runtime.GOMAXPROCS(1)
	runtime.Gosched()
}

複製程式碼

列印結果:

lily
yoyo
cersei
rose
annei
複製程式碼

以上我們得出一個結論,不要對“go函式”的執行時機做任何的假設,除非你確實能做出讓這種假設成為絕對事實的保證。

假設T型別的方法上接收器既有T型別的,又有*T指標型別的,那麼就不可以在不能定址的T值上呼叫*T接收器的方法

請看程式碼,試問能正常編譯通過嗎?

import (
	"fmt"
)
type Lili struct{
	Name string
}

func (Lili *Lili) fmtPointer(){
	fmt.Println("poniter")
}

func (Lili Lili) fmtReference(){
	fmt.Println("reference")
}


func main(){
	li := Lili{}
	li.fmtPointer()
}
複製程式碼

答案:

能正常編譯通過,並輸出"poniter"
複製程式碼

感覺有點詫異,請接著看以下的程式碼,試問能編譯通過?

import (
	"fmt"
)
type Lili struct{
	Name string
}

func (Lili *Lili) fmtPointer(){
	fmt.Println("poniter")
}

func (Lili Lili) fmtReference(){
	fmt.Println("reference")
}


func main(){
	Lili{}.fmtPointer()
}

複製程式碼

答案:

不能編譯通過。
“cannot call pointer method on Lili literal”
“cannot take the address of Lili literal”

複製程式碼

是不是有點奇怪?這是為什麼呢?其實在第一個程式碼示例中,main主函式中的“li”是一個變數,li的雖然是型別Lili,但是li是可以定址的,&li的型別是*Lili,因此可以呼叫*Lili的方法。

一個包含nil指標的介面不是nil介面

請看下列程式碼,試問返回什麼

import (
	"bytes"
	"fmt"
	"io"
)

const debug = true

func main(){
	var buf *bytes.Buffer
	if debug{
		buf = new(bytes.Buffer)
	}
	f(buf)
}
func f(out io.Writer){

	if out != nil{
		fmt.Println("surprise!")
	}
}
複製程式碼

答案是輸出:surprise。 ok,讓我們吧debug開關關掉,及debug的值變為false。那麼輸出什麼呢?是不是什麼都不輸出?

import (
	"bytes"
	"fmt"
	"io"
)

const debug = false

func main(){
	var buf *bytes.Buffer
	if debug{
		buf = new(bytes.Buffer)
	}
	f(buf)
}
func f(out io.Writer){

	if out != nil{
		fmt.Println("surprise!")
	}
}

複製程式碼

答案是:依然輸出surprise。

這是為什麼呢? 這就牽扯到一個概念了,是關於介面值的。概念上講一個介面的值分為兩部分:一部分是型別,一部分是型別對應的值,他們分別叫:動態型別和動態值。型別系統是針對編譯型語言的,型別是編譯期的概念,因此型別不是一個值。 在上述程式碼中,給f函式的out引數賦了一個*bytes.Buffer的空指標,所以out的動態值是nil。然而它的動態型別是*bytes.Buffer,意思是:“A non-nil interface containing a nil pointer”,所以“out!=nil”的結果依然是true。 但是,對於直接的*bytes.Buffer型別的判空不會出現此問題。

import (
	"bytes"
	"fmt"
)

func main(){
	var buf *bytes.Buffer
	if buf == nil{
		fmt.Println("right")
	}
}
複製程式碼

還是輸出: right 只有 介面指標 傳入函式的介面引數時,才會出現以上的坑。 修改起來也很方便,把*bytes.Buffer改為io.Writer就好了。

import (
	"bytes"
	"fmt"
	"io"
)
const debug = false
func main(){
	var buf  io.Writer //原來是var buf *bytes.Buffer
	if debug{
		buf = new(bytes.Buffer)
	}
	f(buf)
}
func f(out io.Writer){
	if out != nil{
		fmt.Println("surprise!")
	}
}
複製程式碼

將map轉化為json字串的時候,json字串中的順序和map賦值順序無關

請看下列程式碼,請問輸出什麼?若為json字串,則json字串中key的順序是什麼?

func main() {
	params := make(map[string]string)

	params["id"] = "1"
	params["id1"] = "3"
	params["controller"] = "sections"

	data, _ := json.Marshal(params)
	fmt.Println(string(data))
}
複製程式碼

答案:輸出{"controller":"sections","id":"1","id1":"3"} 利用Golang自帶的json轉換包轉換,會將map中key的順序改為字母順序,而不是map的賦值順序。map這個結構哪怕利用for range遍歷的時候,其中的key也是無序的,可以理解為map就是個無序的結構,和php中的array要區分開來

Json反序列化數字到interface{}型別的值中,預設解析為float64型別

請看以下程式,程式想要輸出json資料中整型id加上3的值,請問程式會報錯嗎?


func main(){
	jsonStr := `{"id":1058,"name":"RyuGou"}`
	var jsonData map[string]interface{}
	json.Unmarshal([]byte(jsonStr), &jsonData)

	sum :=  jsonData["id"].(int) + 3
	fmt.Println(sum)
}

複製程式碼

答案是會報錯,輸出結果為:

panic: interface conversion: interface {} is float64, not int
複製程式碼

使用 Golang 解析 JSON 格式資料時,若以 interface{} 接收資料,則會按照下列規則進行解析:

bool, for JSON booleans

float64, for JSON numbers

string, for JSON strings

[]interface{}, for JSON arrays

map[string]interface{}, for JSON objects

nil for JSON null

複製程式碼

應該改為:

func main(){
	jsonStr := `{"id":1058,"name":"RyuGou"}`
	var jsonData map[string]interface{}
	json.Unmarshal([]byte(jsonStr), &jsonData)

	sum :=  int(jsonData["id"].(float64)) + 3
	fmt.Println(sum)
}
複製程式碼

即使在有多個變數、且有的變數存在有的變數不存在、且這些變數共同賦值的情況下,也不可以使用:=來給全域性變數賦值

:=往往是用來宣告區域性變數的,在多個變數賦值且有的值存在的情況下,:=也可以用來賦值使用,例如:

msgStr := "hello wolrd"
msgStr, err := "hello", errors.New("xxx")//err並不存在
複製程式碼

但是,假如全域性變數也使用類似的方式賦值,就會出現問題,請看下列程式碼,試問能編譯通過嗎?

var varTest string

func test(){
	varTest, err := function()
	fmt.Println(err.Error())
}

func function()(string, error){
	return "hello world", errors.New("error")
}


func main(){
	test()
}

複製程式碼

答案是:通不過。輸出:

varTest declared and not used
複製程式碼

但是如果改成如下程式碼,就可以通過:

var varTest string

func test(){
	err := errors.New("error")
	varTest, err = function()
	fmt.Println(err.Error())
}

func function()(string, error){
	return "hello world", errors.New("error")
}


func main(){
	test()
}
複製程式碼

輸出:

error
複製程式碼

這是什麼原因呢? 答案其實很簡單,在test方法中,如果使用varTest, err := function()這種方式的話,相當於在函式中又定義了一個和全域性變數varTest名字相同的區域性變數,而這個區域性變數又沒有使用,所以會編譯不通過。

*interface 是一個指向interface的指標型別,而不是interface型別

請問以下程式碼,能編譯通過嗎?

import (
	"fmt"
)

type Father interface {
	Hello()
}


type Child struct {
	Name string
}

func (s Child)Hello()  {

}

func main(){
	var buf  Child
	buf = Child{}
	f(&buf)
}
func f(out *Father){
	if out != nil{
		fmt.Println("surprise!")
	}
}
複製程式碼

答案是:不能編譯通過。輸出:

*Father is pointer to interface, not interface
複製程式碼

注意了:介面型別的變數可以被賦值為實現介面的結構體的例項,但是並不能代表介面的指標可以被賦值為實現介面的結構體的指標例項。即:

var buf Father = Child{}
複製程式碼

是對的,但是

var buf *Father = new(Child)
複製程式碼

卻是不對的。應該改為:

 var buf Father = Child{}
 var pointer *Father = &buf
複製程式碼

要想讓問題最開始的程式碼編譯通過要將以上程式碼修改為:

import (
	"fmt"
)

type Father interface {
	Hello()
}


type Child struct {
	Name string
}

func (s Child)Hello()  {

}

func main(){
	var buf  Father
	buf = Child{}
	f(&buf)
}
func f(out *Father){
	if out != nil{
		fmt.Println("surprise!")
	}
}
複製程式碼

相關文章