您的位置:首页 > 编程语言 > Go语言

[转]Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误-高级

2017-08-25 14:16 489 查看

from:https://levy.at/blog/11

进阶篇

关闭HTTP的响应

level:intermediate

当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。

一些新的Go开发者确实尝试关闭响应主体,但他们在错误的地方做。

packagemain

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

funcmain(){
resp,err:=http.Get("https://api.ipify.org?format=json")
deferresp.Body.Close()//notok
iferr!=nil{
fmt.Println(err)
return
}

body,err:=ioutil.ReadAll(resp.Body)
iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(string(body))
}


这段代码对于成功的请求没问题,但如果http的请求失败,resp变量可能会是nil,这将导致一个runtimepanic。

最常见的关闭响应主体的方法是在http响应的错误检查后调用defer。

packagemain

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

funcmain(){
resp,err:=http.Get("https://api.ipify.org?format=json")
iferr!=nil{
fmt.Println(err)
return
}

deferresp.Body.Close()//ok,mostofthetime:-)
body,err:=ioutil.ReadAll(resp.Body)
iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(string(body))
}


大多数情况下,当你的http响应失败时,resp变量将为nil,而err变量将是non-nil。然而,当你得到一个重定向的错误时,两个变量都将是non-nil。这意味着你最后依然会内存泄露。

通过在http响应错误处理中添加一个关闭non-nil响应主体的的调用来修复这个问题。另一个方法是使用一个defer调用来关闭所有失败和成功的请求的响应主体。

packagemain

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

funcmain(){
resp,err:=http.Get("https://api.ipify.org?format=json")
ifresp!=nil{
deferresp.Body.Close()
}

iferr!=nil{
fmt.Println(err)
return
}

body,err:=ioutil.ReadAll(resp.Body)
iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(string(body))
}


resp.Body.Close()的原始实现也会读取并丢弃剩余的响应主体数据。这确保了http的链接在keepalivehttp连接行为开启的情况下,可以被另一个请求复用。最新的http客户端的行为是不同的。现在读取并丢弃剩余的响应数据是你的职责。如果你不这么做,http的连接可能会关闭,而无法被重用。这个小技巧应该会写在Go1.5的文档中。

如果http连接的重用对你的应用很重要,你可能需要在响应处理逻辑的后面添加像下面的代码:

_,err=io.Copy(ioutil.Discard,resp.Body)


如果你不立即读取整个响应将是必要的,这可能在你处理jsonAPI响应时会发生:

json.NewDecoder(resp.Body).Decode(&data)


关闭HTTP的连接

level:intermediate

一些HTTP服务器保持会保持一段时间的网络连接(根据HTTP1.1的说明和服务器端的“keep-alive”配置)。默认情况下,标准http库只在目标HTTP服务器要求关闭时才会关闭网络连接。这意味着你的应用在某些条件下消耗完sockets/file的描述符。

你可以通过设置请求变量中的Close域的值为true,来让http库在请求完成时关闭连接。

另一个选项是添加一个Connection的请求头,并设置为close。目标HTTP服务器应该也会响应一个Connection:close的头。当http库看到这个响应头时,它也将会关闭连接。

packagemain

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

funcmain(){
req,err:=http.NewRequest("GET","http://golang.org",nil)
iferr!=nil{
fmt.Println(err)
return
}

req.Close=true
//ordothis:
//req.Header.Add("Connection","close")

resp,err:=http.DefaultClient.Do(req)
ifresp!=nil{
deferresp.Body.Close()
}

iferr!=nil{
fmt.Println(err)
return
}

body,err:=ioutil.ReadAll(resp.Body)
iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}


你也可以取消http的全局连接复用。你将需要为此创建一个自定义的http传输配置。

packagemain

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

funcmain(){
tr:=&http.Transport{DisableKeepAlives:true}
client:=&http.Client{Transport:tr}

resp,err:=client.Get("http://golang.org")
ifresp!=nil{
deferresp.Body.Close()
}

iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(resp.StatusCode)

body,err:=ioutil.ReadAll(resp.Body)
iferr!=nil{
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}


如果你向同一个HTTP服务器发送大量的请求,那么把保持网络连接的打开是没问题的。然而,如果你的应用在短时间内向大量不同的HTTP服务器发送一两个请求,那么在引用收到响应后立刻关闭网络连接是一个好主意。增加打开文件的限制数可能也是个好主意。当然,正确的选择源自于应用。

比较Structs,Arrays,Slices,andMaps

level:intermediate

如果结构体中的各个元素都可以用你可以使用等号来比较的话,那就可以使用相号,==,来比较结构体变量。

packagemain

import"fmt"

typedatastruct{
numint
fpfloat32
complexcomplex64
strstring
charrune
yesbool
events<-chanstring
handlerinterface{}
ref*byte
raw[10]byte
}

funcmain(){
v1:=data{}
v2:=data{}
fmt.Println("v1==v2:",v1==v2)//prints:v1==v2:true
}


如果结构体中的元素无法比较,那使用等号将导致编译错误。注意数组仅在它们的数据元素可比较的情况下才可以比较。

packagemain

import"fmt"

typedatastruct{
numint//ok
checks[10]func()bool//notcomparable
doitfunc()bool//notcomparable
mmap[string]string//notcomparable
bytes[]byte//notcomparable
}

funcmain(){
v1:=data{}
v2:=data{}
fmt.Println("v1==v2:",v1==v2)
}


Go确实提供了一些助手函数,用于比较那些无法使用等号比较的变量。

最常用的方法是使用reflect包中的DeepEqual()函数。

packagemain

import(
"fmt"
"reflect"
)

typedatastruct{
numint//ok
checks[10]func()bool//notcomparable
doitfunc()bool//notcomparable
mmap[string]string//notcomparable
bytes[]byte//notcomparable
}

funcmain(){
v1:=data{}
v2:=data{}
fmt.Println("v1==v2:",reflect.DeepEqual(v1,v2))//prints:v1==v2:true

m1:=map[string]string{"one":"a","two":"b"}
m2:=map[string]string{"two":"b","one":"a"}
fmt.Println("m1==m2:",reflect.DeepEqual(m1,m2))//prints:m1==m2:true

s1:=[]int{1,2,3}
s2:=[]int{1,2,3}
fmt.Println("s1==s2:",reflect.DeepEqual(s1,s2))//prints:s1==s2:true
}


除了很慢(这个可能会也可能不会影响你的应用),DeepEqual()也有其他自身的技巧。

packagemain

import(
"fmt"
"reflect"
)

funcmain(){
varb1[]byte=nil
b2:=[]byte{}
fmt.Println("b1==b2:",reflect.DeepEqual(b1,b2))//prints:b1==b2:false
}


DeepEqual()不会认为空的slice与“nil”的slice相等。这个行为与你使用bytes.Equal()函数的行为不同。bytes.Equal()认为“nil”和空的slice是相等的。

packagemain

import(
"fmt"
"bytes"
)

funcmain(){
varb1[]byte=nil
b2:=[]byte{}
fmt.Println("b1==b2:",bytes.Equal(b1,b2))//prints:b1==b2:true
}


DeepEqual()在比较slice时并不总是完美的。

packagemain

import(
"fmt"
"reflect"
"encoding/json"
)

funcmain(){
varstrstring="one"
varininterface{}="one"
fmt.Println("str==in:",str==in,reflect.DeepEqual(str,in))
//prints:str==in:truetrue

v1:=[]string{"one","two"}
v2:=[]interface{}{"one","two"}
fmt.Println("v1==v2:",reflect.DeepEqual(v1,v2))
//prints:v1==v2:false(notok)

data:=map[string]interface{}{
"code":200,
"value":[]string{"one","two"},
}
encoded,_:=json.Marshal(data)
vardecodedmap[string]interface{}
json.Unmarshal(encoded,&decoded)
fmt.Println("data==decoded:",reflect.DeepEqual(data,decoded))
//prints:data==decoded:false(notok)
}


如果你的byteslice(或者字符串)中包含文字数据,而当你要不区分大小写形式的值时(在使用==,bytes.Equal(),或者bytes.Compare()),你可能会尝试使用“bytes”和“string”包中的ToUpper()或者ToLower()函数。对于英语文本,这么做是没问题的,但对于许多其他的语言来说就不行了。这时应该使用strings.EqualFold()和bytes.EqualFold()。

如果你的byteslice中包含需要验证用户数据的隐私信息(比如,加密哈希、tokens等),不要使用reflect.DeepEqual()、bytes.Equal(),或者bytes.Compare(),因为这些函数将会让你的应用易于被定时攻击。为了避免泄露时间信息,使用'crypto/subtle'包中的函数(即,subtle.ConstantTimeCompare())。

从Panic中恢复

level:intermediate

recover()函数可以用于获取/拦截panic。仅当在一个defer函数中被完成时,调用recover()将会完成这个小技巧。

Incorrect:

ackagemain

import"fmt"

funcmain(){
recover()//doesn'tdoanything
panic("notgood")
recover()//won'tbeexecuted:)
fmt.Println("ok")
}


Works:

packagemain

import"fmt"

funcmain(){
deferfunc(){
fmt.Println("recovered:",recover())
}()

panic("notgood")
}


recover()的调用仅当它在defer函数中被直接调用时才有效。

Fails:

packagemain

import"fmt"

funcdoRecover(){
fmt.Println("recovered=>",recover())//prints:recovered=><nil>
}

funcmain(){
deferfunc(){
doRecover()//panicisnotrecovered
}()

panic("notgood")
}


在Slice,Array,andMap"range"语句中更新引用元素的值

level:intermediate

在“range”语句中生成的数据的值是真实集合元素的拷贝。它们不是原有元素的引用。这意味着更新这些值将不会修改原来的数据。同时也意味着使用这些值的地址将不会得到原有数据的指针。

packagemain

import"fmt"

funcmain(){
data:=[]int{1,2,3}
for_,v:=rangedata{
v*=10//originalitemisnotchanged
}

fmt.Println("data:",data)//printsdata:[123]
}


如果你需要更新原有集合中的数据,使用索引操作符来获得数据。

packagemain

import"fmt"

funcmain(){
data:=[]int{1,2,3}
fori,_:=rangedata{
data[i]*=10
}

fmt.Println("data:",data)//printsdata:[102030]
}


如果你的集合保存的是指针,那规则会稍有不同。如果要更新原有记录指向的数据,你依然需要使用索引操作,但你可以使用forrange语句中的第二个值来更新存储在目标位置的数据。

packagemain

import"fmt"

funcmain(){
data:=[]*struct{numint}{{1},{2},{3}}

for_,v:=rangedata{
v.num*=10
}

fmt.Println(data[0],data[1],data[2])//prints&{10}&{20}&{30}
}


在Slice中"隐藏"数据

level:intermediate

当你重新划分一个slice时,新的slice将引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。

packagemain

import"fmt"

funcget()[]byte{
raw:=make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0])//prints:1000010000<byte_addr_x>
returnraw[:3]
}

funcmain(){
data:=get()
fmt.Println(len(data),cap(data),&data[0])//prints:310000<byte_addr_x>
}


为了避免这个陷阱,你需要从临时的slice中拷贝数据(而不是重新划分slice)。

packagemain

import"fmt"

funcget()[]byte{
raw:=make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0])//prints:1000010000<byte_addr_x>
res:=make([]byte,3)
copy(res,raw[:3])
returnres
}

funcmain(){
data:=get()
fmt.Println(len(data),cap(data),&data[0])//prints:33<byte_addr_y>
}


Slice的数据“毁坏”

level:intermediate

比如说你需要重新一个路径(在slice中保存)。你通过修改第一个文件夹的名字,然后把名字合并来创建新的路劲,来重新划分指向各个文件夹的路径。

packagemain

import(
"fmt"
"bytes"
)

funcmain(){
path:=[]byte("AAAA/BBBBBBBBB")
sepIndex:=bytes.IndexByte(path,'/')
dir1:=path[:sepIndex]
dir2:=path[sepIndex+1:]
fmt.Println("dir1=>",string(dir1))//prints:dir1=>AAAA
fmt.Println("dir2=>",string(dir2))//prints:dir2=>BBBBBBBBB

dir1=append(dir1,"suffix"...)
path=bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

fmt.Println("dir1=>",string(dir1))//prints:dir1=>AAAAsuffix
fmt.Println("dir2=>",string(dir2))//prints:dir2=>uffixBBBB(notok)

fmt.Println("newpath=>",string(path))
}


结果与你想的不一样。与"AAAAsuffix/BBBBBBBBB"相反,你将会得到"AAAAsuffix/uffixBBBB"。这个情况的发生是因为两个文件夹的slice都潜在的引用了同一个原始的路径slice。这意味着原始路径也被修改了。根据你的应用,这也许会是个问题。

通过分配新的slice并拷贝需要的数据,你可以修复这个问题。另一个选择是使用完整的slice表达式。

packagemain

import(
"fmt"
"bytes"
)

funcmain(){
path:=[]byte("AAAA/BBBBBBBBB")
sepIndex:=bytes.IndexByte(path,'/')
dir1:=path[:sepIndex:sepIndex]//fullsliceexpression
dir2:=path[sepIndex+1:]
fmt.Println("dir1=>",string(dir1))//prints:dir1=>AAAA
fmt.Println("dir2=>",string(dir2))//prints:dir2=>BBBBBBBBB

dir1=append(dir1,"suffix"...)
path=bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

fmt.Println("dir1=>",string(dir1))//prints:dir1=>AAAAsuffix
fmt.Println("dir2=>",string(dir2))//prints:dir2=>BBBBBBBBB(oknow)

fmt.Println("newpath=>",string(path))
}


完整的slice表达式中的额外参数可以控制新的slice的容量。现在在那个slice后添加元素将会触发一个新的buffer分配,而不是覆盖第二个slice中的数据。

"走味的"Slices

level:intermediate

多个slice可以引用同一个数据。比如,当你从一个已有的slice创建一个新的slice时,这就会发生。如果你的应用功能需要这种行为,那么你将需要关注下“走味的”slice。

在某些情况下,在一个slice中添加新的数据,在原有数组无法保持更多新的数据时,将导致分配一个新的数组。而现在其他的slice还指向老的数组(和老的数据)。

import"fmt"

funcmain(){
s1:=[]int{1,2,3}
fmt.Println(len(s1),cap(s1),s1)//prints33[123]

s2:=s1[1:]
fmt.Println(len(s2),cap(s2),s2)//prints22[23]

fori:=ranges2{s2[i]+=20}

//stillreferencingthesamearray
fmt.Println(s1)//prints[12223]
fmt.Println(s2)//prints[2223]

s2=append(s2,4)

fori:=ranges2{s2[i]+=10}

//s1isnow"stale"
fmt.Println(s1)//prints[12223]
fmt.Println(s2)//prints[323314]
}


类型声明和方法

level:intermediate

当你通过把一个现有(非interface)的类型定义为一个新的类型时,新的类型不会继承现有类型的方法。

Fails:

packagemain

import"sync"

typemyMutexsync.Mutex

funcmain(){
varmtxmyMutex
mtx.Lock()//error
mtx.Unlock()//error
}


CompileErrors:

/tmp/sandbox106401185/main.go:9:mtx.Lockundefined(typemyMutexhasnofieldormethodLock)/tmp/sandbox106401185/main.go:10:mtx.Unlockundefined(typemyMutexhasnofieldormethodUnlock)


如果你确实需要原有类型的方法,你可以定义一个新的struct类型,用匿名方式把原有类型嵌入其中。

Works:

packagemain

import"sync"

typemyLockerstruct{
sync.Mutex
}

funcmain(){
varlockmyLocker
lock.Lock()//ok
lock.Unlock()//ok
}


interface类型的声明也会保留它们的方法集合。

Works:

packagemain

import"sync"

typemyLockersync.Locker

funcmain(){
varlockmyLocker=new(sync.Mutex)
lock.Lock()//ok
lock.Unlock()//ok
}


从"forswitch"和"forselect"代码块中跳出

level:intermediate

没有标签的“break”声明只能从内部的switch/select代码块中跳出来。如果无法使用“return”声明的话,那就为外部循环定义一个标签是另一个好的选择。

packagemain

import"fmt"

funcmain(){
loop:
for{
switch{
casetrue:
fmt.Println("breakingout...")
breakloop
}
}

fmt.Println("out!")
}


"goto"声明也可以完成这个功能。。。

"for"声明中的迭代变量和闭包

level:intermediate

这在Go中是个很常见的技巧。for语句中的迭代变量在每次迭代时被重新使用。这就意味着你在for循环中创建的闭包(即函数字面量)将会引用同一个变量(而在那些goroutine开始执行时就会得到那个变量的值)。

Incorrect:

packagemain

import(
"fmt"
"time"
)

funcmain(){
data:=[]string{"one","two","three"}

for_,v:=rangedata{
gofunc(){
fmt.Println(v)
}()
}

time.Sleep(3*time.Second)
//goroutinesprint:three,three,three
}


最简单的解决方法(不需要修改goroutine)是,在for循环代码块内把当前迭代的变量值保存到一个局部变量中。

Works:

packagemain

import(
"fmt"
"time"
)

funcmain(){
data:=[]string{"one","two","three"}

for_,v:=rangedata{
vcopy:=v//
gofunc(){
fmt.Println(vcopy)
}()
}

time.Sleep(3*time.Second)
//goroutinesprint:one,two,three
}


另一个解决方法是把当前的迭代变量作为匿名goroutine的参数。

Works:

packagemain

import(
"fmt"
"time"
)

funcmain(){
data:=[]string{"one","two","three"}

for_,v:=rangedata{
gofunc(instring){
fmt.Println(in)
}(v)
}

time.Sleep(3*time.Second)
//goroutinesprint:one,two,three
}


下面这个陷阱稍微复杂一些的版本。

Incorrect:

packagemain

import(
"fmt"
"time"
)

typefieldstruct{
namestring
}

func(p*field)print(){
fmt.Println(p.name)
}

funcmain(){
data:=[]field{{"one"},{"two"},{"three"}}

for_,v:=rangedata{
gov.print()
}

time.Sleep(3*time.Second)
//goroutinesprint:three,three,three
}


Works:

packagemain

import(
"fmt"
"time"
)

typefieldstruct{
namestring
}

func(p*field)print(){
fmt.Println(p.name)
}

funcmain(){
data:=[]field{{"one"},{"two"},{"three"}}

for_,v:=rangedata{
v:=v
gov.print()
}

time.Sleep(3*time.Second)
//goroutinesprint:one,two,three
}


在运行这段代码时你认为会看到什么结果?(原因是什么?)

packagemain

import(
"fmt"
"time"
)

typefieldstruct{
namestring
}

func(p*field)print(){
fmt.Println(p.name)
}

funcmain(){
data:=[]*field{{"one"},{"two"},{"three"}}

for_,v:=rangedata{
gov.print()
}

time.Sleep(3*time.Second)
}


Defer函数调用参数的求值

level:intermediate

被defer的函数的参数会在defer声明时求值(而不是在函数实际执行时)。
Argumentsforadeferredfunctioncallareevaluatedwhenthedeferstatementisevaluated(notwhenthefunctionisactuallyexecuting).

packagemain

import"fmt"

funcmain(){
variint=1

deferfmt.Println("result=>",func()int{returni*2}())
i++
//prints:result=>2(notokifyouexpected4)
}


被Defer的函数调用执行

level:intermediate

被defer的调用会在包含的函数的末尾执行,而不是包含代码块的末尾。对于Go新手而言,一个很常犯的错误就是无法区分被defer的代码执行规则和变量作用规则。如果你有一个长时运行的函数,而函数内有一个for循环试图在每次迭代时都defer资源清理调用,那就会出现问题。

packagemain

import(
"fmt"
"os"
"path/filepath"
)

funcmain(){
iflen(os.Args)!=2{
os.Exit(-1)
}

start,err:=os.Stat(os.Args[1])
iferr!=nil||!start.IsDir(){
os.Exit(-1)
}

vartargets[]string
filepath.Walk(os.Args[1],func(fpathstring,fios.FileInfo,errerror)error{
iferr!=nil{
returnerr
}

if!fi.Mode().IsRegular(){
returnnil
}

targets=append(targets,fpath)
returnnil
})

for_,target:=rangetargets{
f,err:=os.Open(target)
iferr!=nil{
fmt.Println("badtarget:",target,"error:",err)//printserror:toomanyopenfiles
break
}
deferf.Close()//willnotbeclosedattheendofthiscodeblock
//dosomethingwiththefile...
}
}


解决这个问题的一个方法是把代码块写成一个函数。

packagemain

import(
"fmt"
"os"
"path/filepath"
)

funcmain(){
iflen(os.Args)!=2{
os.Exit(-1)
}

start,err:=os.Stat(os.Args[1])
iferr!=nil||!start.IsDir(){
os.Exit(-1)
}

vartargets[]string
filepath.Walk(os.Args[1],func(fpathstring,fios.FileInfo,errerror)error{
iferr!=nil{
returnerr
}

if!fi.Mode().IsRegular(){
returnnil
}

targets=append(targets,fpath)
returnnil
})

for_,target:=rangetargets{
func(){
f,err:=os.Open(target)
iferr!=nil{
fmt.Println("badtarget:",target,"error:",err)
return
}
deferf.Close()//ok
//dosomethingwiththefile...
}()
}
}


另一个方法是去掉defer语句:-)

失败的类型断言

level:intermediate

失败的类型断言返回断言声明中使用的目标类型的“零值”。这在与隐藏变量混合时,会发生未知情况。

Incorrect:

packagemain

import"fmt"

funcmain(){
vardatainterface{}="great"

ifdata,ok:=data.(int);ok{
fmt.Println("[isanint]value=>",data)
}else{
fmt.Println("[notanint]value=>",data)
//prints:[notanint]value=>0(not"great")
}
}


Works:

packagemain

import"fmt"

funcmain(){
vardatainterface{}="great"

ifres,ok:=data.(int);ok{
fmt.Println("[isanint]value=>",res)
}else{
fmt.Println("[notanint]value=>",data)
//prints:[notanint]value=>great(asexpected)
}
}


阻塞的Goroutine和资源泄露

level:intermediate

RobPike在2012年的GoogleI/O大会上所做的“GoConcurrencyPatterns”的演讲上,说道过几种基础的并发模式。从一组目标中获取第一个结果就是其中之一。

funcFirst(querystring,replicas...Search)Result{
c:=make(chanResult)
searchReplica:=func(iint){c<-replicas[i](query)}
fori:=rangereplicas{
gosearchReplica(i)
}
return<-c
}


这个函数在每次搜索重复时都会起一个goroutine。每个goroutine把它的搜索结果发送到结果的channel中。结果channel的第一个值被返回。

那其他goroutine的结果会怎样呢?还有那些goroutine自身呢?

在First()函数中的结果channel是没缓存的。这意味着只有第一个goroutine返回。其他的goroutine会困在尝试发送结果的过程中。这意味着,如果你有不止一个的重复时,每个调用将会泄露资源。

为了避免泄露,你需要确保所有的goroutine退出。一个不错的方法是使用一个有足够保存所有缓存结果的channel。

funcFirst(querystring,replicas...Search)Result{
c:=make(chanResult,len(replicas))
searchReplica:=func(iint){c<-replicas[i](query)}
fori:=rangereplicas{
gosearchReplica(i)
}
return<-c
}


另一个不错的解决方法是使用一个有default情况的select语句和一个保存一个缓存结果的channel。default情况保证了即使当结果channel无法收到消息的情况下,goroutine也不会堵塞。

funcFirst(querystring,replicas...Search)Result{
c:=make(chanResult,1)
searchReplica:=func(iint){
select{
casec<-replicas[i](query):
default:
}
}
fori:=rangereplicas{
gosearchReplica(i)
}
return<-c
}


你也可以使用特殊的取消channel来终止workers。

funcFirst(querystring,replicas...Search)Result{
c:=make(chanResult)
done:=make(chanstruct{})
deferclose(done)
searchReplica:=func(iint){
select{
casec<-replicas[i](query):
case<-done:
}
}
fori:=rangereplicas{
gosearchReplica(i)
}

return<-c
}


为何在演讲中会包含这些bug?RobPike仅仅是不想把演示复杂化。这么作是合理的,但对于Go新手而言,可能会直接使用代码,而不去思考它可能有问题。

高级篇

使用指针接收方法的值的实例

level:advanced

只要值是可取址的,那在这个值上调用指针接收方法是没问题的。换句话说,在某些情况下,你不需要在有一个接收值的方法版本。

然而并不是所有的变量是可取址的。Map的元素就不是。通过interface引用的变量也不是。

packagemain

import"fmt"

typedatastruct{
namestring
}

func(p*data)print(){
fmt.Println("name:",p.name)
}

typeprinterinterface{
print()
}

funcmain(){
d1:=data{"one"}
d1.print()//ok

varinprinter=data{"two"}//error
in.print()

m:=map[string]data{"x":data{"three"}}
m["x"].print()//error
}


CompileErrors:

/tmp/sandbox017696142/main.go:21:cannotusedataliteral(typedata)astypeprinterinassignment:datadoesnotimplementprinter(printmethodhaspointerreceiver)
/tmp/sandbox017696142/main.go:25:cannotcallpointermethodonm["x"]/tmp/sandbox017696142/main.go:25:cannottaketheaddressofm["x"]


更新Map的值

level:advanced

如果你有一个struct值的map,你无法更新单个的struct值。

Fails:

packagemain

typedatastruct{
namestring
}

funcmain(){
m:=map[string]data{"x":{"one"}}
m["x"].name="two"//error
}


CompileError:

/tmp/sandbox380452744/main.go:9:cannotassigntom["x"].name


这个操作无效是因为map元素是无法取址的。

而让Go新手更加困惑的是slice元素是可以取址的。

packagemain

import"fmt"

typedatastruct{
namestring
}

funcmain(){
s:=[]data{{"one"}}
s[0].name="two"//ok
fmt.Println(s)//prints:[{two}]
}


注意在不久之前,使用编译器之一(gccgo)是可以更新map的元素值的,但这一行为很快就被修复了:-)它也被认为是Go1.3的潜在特性。在那时还不是要急需支持的,但依旧在todolist中。

第一个有效的方法是使用一个临时变量。

packagemain

import"fmt"

typedatastruct{
namestring
}

funcmain(){
m:=map[string]data{"x":{"one"}}
r:=m["x"]
r.name="two"
m["x"]=r
fmt.Printf("%v",m)//prints:map[x:{two}]
}


另一个有效的方法是使用指针的map。

packagemain

import"fmt"

typedatastruct{
namestring
}

funcmain(){
m:=map[string]*data{"x":{"one"}}
m["x"].name="two"//ok
fmt.Println(m["x"])//prints:&{two}
}


顺便说下,当你运行下面的代码时会发生什么?

packagemain

typedatastruct{
namestring
}

funcmain(){
m:=map[string]*data{"x":{"one"}}
m["z"].name="what?"//???
}


"nil"Interfaces和"nil"Interfaces的值

level:advanced

这在Go中是第二最常见的技巧,因为interface虽然看起来像指针,但并不是指针。interface变量仅在类型和值为“nil”时才为“nil”。

interface的类型和值会根据用于创建对应interface变量的类型和值的变化而变化。当你检查一个interface变量是否等于“nil”时,这就会导致未预期的行为。

packagemain

import"fmt"

funcmain(){
vardata*byte
varininterface{}

fmt.Println(data,data==nil)//prints:<nil>true
fmt.Println(in,in==nil)//prints:<nil>true

in=data
fmt.Println(in,in==nil)//prints:<nil>false
//'data'is'nil',but'in'isnot'nil'
}


当你的函数返回interface时,小心这个陷阱。

Incorrect:

packagemain

import"fmt"

funcmain(){
doit:=func(argint)interface{}{
varresult*struct{}=nil

if(arg>0){
result=&struct{}{}
}

returnresult
}

ifres:=doit(-1);res!=nil{
fmt.Println("goodresult:",res)//prints:goodresult:<nil>
//'res'isnot'nil',butitsvalueis'nil'
}
}


Works:

packagemain

import"fmt"

funcmain(){
doit:=func(argint)interface{}{
varresult*struct{}=nil

if(arg>0){
result=&struct{}{}
}else{
returnnil//returnanexplicit'nil'
}

returnresult
}

ifres:=doit(-1);res!=nil{
fmt.Println("goodresult:",res)
}else{
fmt.Println("badresult(resisnil)")//hereasexpected
}
}


栈和堆变量

level:advanced

你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用new()或者make()函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和“泄露分析”的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。

如果你想知道变量分配的位置,在“gobuild”或“gorun”上传入“-m“gc标志(即,gorun-gcflags-mapp.go)。

GOMAXPROCS,并发,和并行

level:advanced

默认情况下,Go仅使用一个执行上下文/OS线程(在当前的版本)。这个数量可以通过设置GOMAXPROCS来提高。

一个常见的误解是,GOMAXPROCS表示了CPU的数量,Go将使用这个数量来运行goroutine。而runtime.GOMAXPROCS()函数的文档让人更加的迷茫。GOMAXPROCS变量描述(TheGoProgrammingLanguage)所讨论OS线程的内容比较好。

你可以设置GOMAXPROCS的数量大于CPU的数量。GOMAXPROCS的最大值是256。

packagemain

import(
"fmt"
"runtime"
)

funcmain(){
fmt.Println(runtime.GOMAXPROCS(-1))//prints:1
fmt.Println(runtime.NumCPU())//prints:1(onplay.golang.org)
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1))//prints:20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1))//prints:256
}


读写操作的重排顺序

level:advanced

Go可能会对某些操作进行重新排序,但它能保证在一个goroutine内的所有行为顺序是不变的。然而,它并不保证多goroutine的执行顺序。

packagemain

import(
"runtime"
"time"
)

var_=runtime.GOMAXPROCS(3)

vara,bint

funcu1(){
a=1
b=2
}

funcu2(){
a=3
b=4
}

funcp(){
println(a)
println(b)
}

funcmain(){
gou1()
gou2()
gop()
time.Sleep(1*time.Second)
}


如果你多运行几次上面的代码,你可能会发现a和b变量有多个不同的组合:

1
2

3
4

0
2

0
0

1
4


a和b最有趣的组合式是"02"。这表明b在a之前更新了。

如果你需要在多goroutine内放置读写顺序的变化,你将需要使用channel,或者使用"sync"包构建合适的结构体。

优先调度

level:advanced

有可能会出现这种情况,一个无耻的goroutine阻止其他goroutine运行。当你有一个不让调度器运行的for循环时,这就会发生。

packagemain

import"fmt"

funcmain(){
done:=false

gofunc(){
done=true
}()

for!done{
}
fmt.Println("done!")
}


for循环并不需要是空的。只要它包含了不会触发调度执行的代码,就会发生这种问题。

调度器会在GC、“go”声明、阻塞channel操作、阻塞系统调用和lock操作后运行。它也会在非内联函数调用后执行。

packagemain

import"fmt"

funcmain(){
done:=false

gofunc(){
done=true
}()

for!done{
fmt.Println("notdone!")//notinlined
}
fmt.Println("done!")
}


要想知道你在for循环中调用的函数是否是内联的,你可以在“gobuild”或“gorun”时传入“-m”gc标志(如,gobuild-gcflags-m)。

另一个选择是显式的唤起调度器。你可以使用“runtime”包中的Goshed()函数。

packagemain

import(
"fmt"
"runtime"
)

funcmain(){
done:=false

gofunc(){
done=true
}()

for!done{
runtime.Gosched()
}
fmt.Println("done!")
}


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: