golang学习笔记说明 表示需要重点关注的的地方 包packagemain导入包import(fmtmathrand)funcmain(){fmt。Println(Myfavoritenumberis,rand。Intn(10))} 函数packagemainimportfmt函数常规定义funcadd(xint,yint)int{returnxy}funcmain(){fmt。Println(add(42,13))}packagemainimportfmt当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。funcadd(x,yint)int{returnxy}funcmain(){fmt。Println(add(42,13))}packagemainimportfmt函数可以返回任意数量的返回值funcswap(x,ystring)(string,string){returny,x}funcmain(){a,b:swap(hello,world)fmt。Println(a,b)}packagemainimportfmtGo的返回值可被命名,它们会被视作定义在函数顶部的变量。这里x,y是返回值funcsplit(sumint)(x,yint){xsum49ysumxreturn没有参数的return语句返回已命名的返回值。也就是直接返回。}funcmain(){fmt。Println(split(17))} 变量packagemainimportfmtvar语句可以出现在包或函数级别全局变量varc,python,javaboolfuncmain(){variintfmt。Println(i,c,python,java)}packagemainimportfmt变量声明可以包含初始值,每个变量对应一个。vari,jint1,2funcmain(){如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。varc,python,javatrue,false,no!fmt。Println(i,j,c,python,java)}packagemainimportfmt注意:函数外的每个语句都必须以关键字开始(var,func等等),因此:结构不能在函数外使用。funcmain(){vari,jint1,2在函数中,简洁赋值语句:可在类型明确的地方代替var声明k:3c,python,java:true,false,no!fmt。Println(i,j,k,c,python,java)} 基本类型packagemainimport(fmtmathcmplx)数据类型:boolstring注意字符串是一个基本类型intint8int16int32int64uintuint8uint16uint32uint64uintptrbyteuint8的别名runeint32的别名表示一个Unicode码点float32float64complex64complex128注意:int,uint和uintptr在32位系统上通常为32位宽,在64位系统上则为64位宽。var(ToBeboolfalseMaxIntuint641641zcomplex128cmplx。Sqrt(512i))funcmain(){fmt。Printf(Type:TValue:v,ToBe,ToBe)T输出数据类型fmt。Printf(Type:TValue:v,MaxInt,MaxInt)fmt。Printf(Type:TValue:v,z,z)}运行结果:Type:boolValue:falseType:uint64Value:18446744073709551615Type:complex128Value:(23i) 零值packagemainimportfmt没有明确初始值的变量声明会被赋予它们的零值。常见数据类型零值是:数值类型为0,布尔类型为false,字符串为(空字符串)。funcmain(){variintvarffloat64varbboolvarsstringfmt。Printf(vvvq,i,f,b,s)} 类型转换packagemainimport(fmtmath)表达式T(v)将值v转换为类型T。一些关于数值的转换:variint42varffloat64float64(i)varuuintuint(f)或者,更加简单的形式:i:42f:float64(i)u:uint(f)与C不同的是,Go在不同类型的项之间赋值时需要显式转换。funcmain(){varx,yint3,4varffloat64math。Sqrt(float64(xxyy))varzuintuint(f)fmt。Println(x,y,z)} 类型推导packagemainimportfmt在声明一个变量而不指定其类型时(即使用不带类型的:语法或var表达式语法),变量的类型由右值推导得出。当右值声明了类型时,新变量的类型与其相同:variintj:ij也是一个int注意:不过当右边包含未指明类型的数值常量时,新变量的类型就可能是int,float64或complex128了,这取决于常量的精度:i:42intf:3。142float64g:0。8670。5icomplex128尝试修改示例代码中v的初始值,并观察它是如何影响类型的。funcmain(){v:42。1修改这里!fmt。Printf(visoftypeT,v)} 常量packagemainimportfmt常量的声明与变量类似,只不过是使用const关键字。常量可以是字符、字符串、布尔值或数值。常量不能用:语法声明。constPi3。14funcmain(){constWorld世界fmt。Println(Hello,World)fmt。Println(Happy,Pi,Day)constTruthtruefmt。Println(Gorules?,Truth)} 数值常量packagemainimportfmt数值常量是高精度的值。一个未指定类型的常量由上下文来决定其类型。再尝试一下输出needInt(Big)吧。(int类型最大可以存储一个64位的整数,有时会更小。)(int可以存放最大64位的整数,根据平台不同有时会更少。)const(将1左移100位来创建一个非常大的数字即这个数的二进制是1后面跟着100个0Big1100再往右移99位,即Small11,或者说Small2SmallBig99)funcneedInt(xint)int{returnx101}funcneedFloat(xfloat64)float64{returnx0。1}funcmain(){fmt。Println(needInt(Small))fmt。Println(needFloat(Small))fmt。Println(needFloat(Big))}结果:210。21。2676506002282295e29 forpackagemainimportfmtGo只有一种循环结构:for循环。基本的for循环由三部分组成,它们用分号隔开:初始化语句:在第一次迭代前执行条件表达式:在每次迭代前求值后置语句:在每次迭代的结尾执行初始化语句通常为一句短变量声明,该变量声明仅在for语句的作用域中可见。一旦条件表达式的布尔值为false,循环迭代就会终止。注意:和C、Java、JavaScript之类的语言不同,Go的for语句后面的三个构成部分外没有小括号,大括号{}则是必须的。funcmain(){sum:0fori:0;i10;i{sumi}fmt。Println(sum)}packagemainimportfmt初始化语句和后置语句是可以省略的funcmain(){sum:1for;sum1000;{sumsum}fmt。Println(sum)} for是Go中的whilepackagemainimportfmt此时你可以去掉分号,因为C的while在Go中叫做forfuncmain(){sum:1forsum1000{sumsum}fmt。Println(sum)} 无限循环packagemain如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑funcmain(){for{}} ifpackagemainimport(fmtmath)Go的if语句与for循环类似,表达式外无需小括号(),而大括号{}则是必须的。funcsqrt(xfloat64)string{ifx0{returnsqrt(x)i}returnfmt。Sprint(math。Sqrt(x))}funcmain(){fmt。Println(sqrt(2),sqrt(4))} if的简短语句packagemainimport(fmtmath)同for一样,if语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在if之内。funcpow(x,n,limfloat64)float64{ifv:math。Pow(x,n);vlim{returnv}returnlim}funcmain(){fmt。Println(pow(3,2,10),pow(3,3,20),)}结果:920 if和elsepackagemainimport(fmtmath)在if的简短语句中声明的变量同样可以在任何对应的else块中使用funcpow(x,n,limfloat64)float64{ifv:math。Pow(x,n);vlim{returnv}else{fmt。Printf(gg,v,lim)}这里开始就不能使用v了returnlim}funcmain(){fmt。Println(pow(3,2,10),pow(3,3,20),)} switchpackagemainimport(fmtruntime)switch是编写一连串ifelse语句的简便方法。它运行第一个值等于条件表达式的case语句。Go的switch语句类似于C、C、Java、JavaScript和PHP中的,不过Go只运行选定的case,而非之后所有的case。实际上,Go自动提供了在这些语言中每个case后面所需的break语句。除非以fallthrough语句结束,否则分支会自动终止。Go的另一点重要的不同在于switch的case无需为常量,且取值不必为整数。funcmain(){fmt。Print(Gorunson)switchos:runtime。GOOS;os{casedarwin:fmt。Println(OSX。)caselinux:fmt。Println(Linux。)default:freebsd,openbsd,plan9,windows。。。fmt。Printf(s。,os)}} switch的求值顺序packagemainimport(fmttime)switch的case语句从上到下顺次执行,直到匹配成功时停止。(例如:switchi{case0:casef():}在i0时f不会被调用。)funcmain(){fmt。Println(WhensSaturday?)today:time。Now()。Weekday()switchtime。Saturday{casetoday0:fmt。Println(Today。)casetoday1:fmt。Println(Tomorrow。)casetoday2:fmt。Println(Intwodays。)default:fmt。Println(Toofaraway。)}} 没有条件的switchpackagemainimport(fmttime)没有条件的switch同switchtrue一样,这种形式能将一长串ifthenelse写得更加清晰。funcmain(){t:time。Now()switch{caset。Hour()12:fmt。Println(Goodmorning!)caset。Hour()17:fmt。Println(Goodafternoon。)default:fmt。Println(Goodevening。)}} deferpackagemainimportfmtdefer语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。funcmain(){deferfmt。Println(world)fmt。Println(hello)} defer栈packagemainimportfmtfuncmain(){fmt。Println(counting)fori:0;i10;i{deferfmt。Println(i)}fmt。Println(done)}结果:countingdone9876543210 指针packagemainimportfmtGo拥有指针。指针保存了值的内存地址。类型T是指向T类型值的指针。其零值为nil。varpint操作符会生成一个指向其操作数的指针。i:42pi操作符表示指针指向的底层值。fmt。Println(p)通过指针p读取ip21通过指针p设置i这也就是通常所说的间接引用或重定向。注意:与C不同,Go没有指针运算。funcmain(){i,j:42,2701p:i指向ifmt。Println(p)通过指针读取i的值p21通过指针设置i的值fmt。Println(i)查看i的值pj指向jpp37通过指针对j进行除法运算fmt。Println(j)查看j的值} 结构体packagemainimportfmt一个结构体(struct)就是一组字段(field)typeVertexstruct{XintYint}funcmain(){fmt。Println(Vertex{1,2})} 结构体字段packagemainimportfmt结构体字段使用点号来访问typeVertexstruct{XintYint}funcmain(){v:Vertex{1,2}v。X4fmt。Println(v。X)} 结构体指针packagemainimportfmt结构体字段可以通过结构体指针来访问。如果我们有一个指向结构体的指针p,那么可以通过(p)。X来访问其字段X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写p。X就可以。typeVertexstruct{XintYint}funcmain(){v:Vertex{1,2}p:vp。X1e9fmt。Println(v)} 结构体文法packagemainimportfmt结构体文法通过直接列出字段的值来新分配一个结构体。使用Name:语法可以仅列出部分字段。(字段名的顺序无关。)特殊的前缀返回一个指向结构体的指针typeVertexstruct{X,Yint}var(v1Vertex{1,2}创建一个Vertex类型的结构体v2Vertex{X:1}Y:0被隐式地赋予v3Vertex{}X:0Y:0pVertex{1,2}创建一个Vertex类型的结构体(指针))funcmain(){fmt。Println(v1,p,v2,v3)} 数组packagemainimportfmt类型〔n〕T表示拥有n个T类型的值的数组。表达式:vara〔10〕int会将变量a声明为拥有10个整数的数组。数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go提供了更加便利的方式来使用数组。funcmain(){vara〔2〕stringa〔0〕Helloa〔1〕Worldfmt。Println(a〔0〕,a〔1〕)fmt。Println(a)primes:〔6〕int{2,3,5,7,11,13}fmt。Println(primes)} 切片packagemainimportfmt每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。类型〔〕T表示一个元素类型为T的切片。切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:a〔low:high〕它会选择一个半开区间,包括第一个元素,但排除最后一个元素。以下表达式创建了一个切片,它包含a中下标从1到3的元素:a〔1:4〕funcmain(){primes:〔6〕int{2,3,5,7,11,13}vars〔〕intprimes〔1:4〕fmt。Println(s)} 切片就像数组的引用packagemainimportfmt切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改funcmain(){names:〔4〕string{John,Paul,George,Ringo,}fmt。Println(names)a:names〔0:2〕b:names〔1:3〕fmt。Println(a,b)b〔0〕XXXfmt。Println(a,b)fmt。Println(names)}结果:〔JohnPaulGeorgeRingo〕〔JohnPaul〕〔PaulGeorge〕〔JohnXXX〕〔XXXGeorge〕〔JohnXXXGeorgeRingo〕 切片文法packagemainimportfmt切片文法类似于没有长度的数组文法。这是一个数组文法:〔3〕bool{true,true,false}下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:〔〕bool{true,true,false}funcmain(){q:〔〕int{2,3,5,7,11,13}fmt。Println(q)r:〔〕bool{true,false,true,true,false,true}fmt。Println(r)s:〔〕struct{iintbbool}{{2,true},{3,false},{5,true},{7,true},{11,false},{13,true},}fmt。Println(s)} 切片的默认行为packagemainimportfmt在进行切片时,你可以利用它的默认行为来忽略上下界。切片下界的默认值为0,上界则是该切片的长度。对于数组vara〔10〕int来说,以下切片是等价的:a〔0:10〕a〔:10〕a〔0:〕a〔:〕funcmain(){s:〔〕int{2,3,5,7,11,13}ss〔1:4〕fmt。Println(s)ss〔:2〕fmt。Println(s)ss〔1:〕fmt。Println(s)} 切片的长度与容量packagemainimportfmt切片拥有长度和容量。切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。切片s的长度和容量可通过表达式len(s)和cap(s)来获取。funcmain(){s:〔〕int{2,3,5,7,11,13}printSlice(s)截取切片使其长度为0ss〔:0〕printSlice(s)拓展其长度ss〔:4〕printSlice(s)舍弃前两个值ss〔2:〕printSlice(s)}funcprintSlice(s〔〕int){fmt。Printf(lendcapdv,len(s),cap(s),s)}结果:len6cap6〔23571113〕len0cap6〔〕len4cap6〔2357〕len2cap4〔57〕 nil切片packagemainimportfmt切片的零值是nil。nil切片的长度和容量为0且没有底层数组。funcmain(){vars〔〕intfmt。Println(s,len(s),cap(s))ifsnil{fmt。Println(nil!)}} 用make创建切片packagemainimportfmt切片可以用内建函数make来创建,这也是你创建动态数组的方式。make函数会分配一个元素为零值的数组并返回一个引用了它的切片:a:make(〔〕int,5)len(a)5要指定它的容量,需向make传入第三个参数:b:make(〔〕int,0,5)len(b)0,cap(b)5bb〔:cap(b)〕len(b)5,cap(b)5bb〔1:〕len(b)4,cap(b)4funcmain(){a:make(〔〕int,5)printSlice(a,a)b:make(〔〕int,0,5)printSlice(b,b)c:b〔:2〕printSlice(c,c)d:c〔2:5〕printSlice(d,d)}funcprintSlice(sstring,x〔〕int){fmt。Printf(slendcapdv,s,len(x),cap(x),x)} 切片的切片packagemainimport(fmtstrings)切片可包含任何类型,甚至包括其它的切片funcmain(){创建一个井字板(经典游戏)board:〔〕〔〕string{〔〕string{,,},〔〕string{,,},〔〕string{,,},}两个玩家轮流打上X和Oboard〔0〕〔0〕Xboard〔2〕〔2〕Oboard〔1〕〔2〕Xboard〔1〕〔0〕Oboard〔0〕〔2〕Xfori:0;ilen(board);i{fmt。Printf(s,strings。Join(board〔i〕,))}} 向切片追加元素packagemainimportfmt为切片追加新的元素是种常用的操作,为此Go提供了内建的append函数。内建函数的文档对此函数有详细的介绍。funcappend(s〔〕T,vs。。。T)〔〕Tappend的第一个参数s是一个元素类型为T的切片,其余类型为T的值将会追加到该切片的末尾。append的结果是一个包含原切片所有元素加上新添加元素的切片。当s的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。funcmain(){vars〔〕intprintSlice(s)添加一个空切片sappend(s,0)printSlice(s)这个切片会按需增长sappend(s,1)printSlice(s)可以一次性添加多个元素sappend(s,2,3,4)printSlice(s)}funcprintSlice(s〔〕int){fmt。Printf(lendcapdv,len(s),cap(s),s)}结果:len0cap0〔〕len1cap1〔0〕len2cap2〔01〕len5cap6〔01234〕 切片的详细介绍 https:blog。gozh。orggoslicesusageandinternals数组 Go的切片是在数组之上的抽象数据类型,因此在了解切片之前必须要先理解数组。 数组类型定义了长度和元素类型。例如,〔4〕int类型表示一个四个整数的数组。数组的长度是固定的,长度是数组类型的一部分(〔4〕int和〔5〕int是完全不同的类型)。数组可以以常规的索引方式访问,表达式s〔n〕访问数组的第n个元素。vara〔4〕inta〔0〕1i:a〔0〕i1 数组不需要显式的初始化;数组的零值是可以直接使用的,数组元素会自动初始化为其对应类型的零值:a〔2〕0,int类型的零值 类型〔4〕int对应内存中四个连续的整数: Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像C语言的数组)。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。)可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。 数组的字面值像这样:b:〔2〕string{Penn,Teller} 当然,也可以让编译器统计数组字面值中元素的数目:b:〔。。。〕string{Penn,Teller} 这两种写法,b都是对应〔2〕string类型。 数组虽然有适用它们的地方,但是数组不够灵活,因此在Go代码中数组使用的并不多。但是,切片则使用得相当广泛。切片基于数组构建,但是提供更强的功能和便利。 切片类型的写法是〔〕T,T是切片元素的类型。和数组不同的是,切片类型并没有给定固定的长度。 切片的字面值和数组字面值很像,不过切片没有指定元素个数:letters:〔〕string{a,b,c,d} 切片可以使用内置函数make创建,函数签名为:funcmake(〔〕T,len,cap)〔〕T 其中T代表被创建的切片元素的类型。函数make接受一个类型、一个长度和一个可选的容量参数。调用make时,内部会分配一个数组,然后返回数组对应的切片。vars〔〕bytesmake(〔〕byte,5,5)s〔〕byte{0,0,0,0,0} 当容量参数被忽略时,它默认为指定的长度。下面是简洁的写法:s:make(〔〕byte,5) 可以使用内置函数len和cap获取切片的长度和容量信息。len(s)5cap(s)5 接下来的两个小节将讨论长度和容量之间的关系。 切片的零值为nil。对于切片的零值,len和cap都将返回0。 切片也可以基于现有的切片或数组生成。切分的范围由两个由冒号分割的索引对应的半开区间指定。例如,表达式b〔1:4〕创建的切片引用数组b的第1到3个元素空间(对应切片的索引为0到2)。b:〔〕byte{g,o,l,a,n,g}b〔1:4〕〔〕byte{o,l,a},sharingthesamestorageasb 切片的开始和结束的索引都是可选的;它们分别默认为零和数组的长度。b〔:2〕〔〕byte{g,o}b〔2:〕〔〕byte{l,a,n,g}b〔:〕b 下面语法也是基于数组创建一个切片:x:〔3〕string{,,}s:x〔:〕aslicereferencingthestorageofx 一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度,和容量(片段的最大长度)。 前面使用make(〔〕byte,5)创建的切片变量s的结构如下: 长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)。关于长度和容量和区域将在下一个例子说明。 我们继续对s进行切片,观察切片的数据结构和它引用的底层数组:ss〔2:4〕 切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。d:〔〕byte{r,o,a,d}e:d〔2:〕e〔〕byte{a,d}e〔1〕me〔〕byte{a,m}d〔〕byte{r,o,a,m} 前面创建的切片s长度小于它的容量。我们可以增长切片的长度为它的容量:ss〔:cap(s)〕 切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。 要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。整个技术是一些支持动态数组语言的常见实现。下面的例子将切片s容量翻倍,先创建一个2倍容量的新切片t,复制s的元素到t,然后将t赋值给s:t:make(〔〕byte,len(s),(cap(s)1)2)1incasecap(s)0fori:ranges{t〔i〕s〔i〕}st 循环中复制的操作可以由copy内置函数替代。copy函数将源切片的元素复制到目的切片。它返回复制元素的数目。funccopy(dst,src〔〕T)int copy函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。此外,copy函数可以正确处理源和目的切片有重叠的情况。 使用copy函数,我们可以简化上面的代码片段:t:make(〔〕byte,len(s),(cap(s)1)2)copy(t,s)st 一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部,必要的话会增加切片的容量,最后返回更新的切片:funcAppendByte(slice〔〕byte,data。。。byte)〔〕byte{m:len(slice)n:mlen(data)ifncap(slice){ifnecessary,reallocateallocatedoublewhatsneeded,forfuturegrowth。newSlice:make(〔〕byte,(n1)2)copy(newSlice,slice)slicenewSlice}sliceslice〔0:n〕copy(slice〔m:n〕,data)returnslice} 下面是AppendByte的一种用法:p:〔〕byte{2,3,5}pAppendByte(p,7,11,13)p〔〕byte{2,3,5,7,11,13} 类似AppendByte的函数比较实用,因为它提供了切片容量增长的完全控制。根据程序的特点,可能希望分配较小的活较大的块,或则是超过某个大小再分配。 但大多数程序不需要完全的控制,因此Go提供了一个内置函数append,用于大多数场合;它的函数签名:funcappend(s〔〕T,x。。。T)〔〕T append函数将x追加到切片s的末尾,并且在必要的时候增加容量。a:make(〔〕int,1)a〔〕int{0}aappend(a,1,2,3)a〔〕int{0,1,2,3} 如果是要将一个切片追加到另一个切片尾部,需要使用。。。语法将第2个参数展开为参数列表。a:〔〕string{John,Paul}b:〔〕string{George,Ringo,Pete}aappend(a,b。。。)equivalenttoappend(a,b〔0〕,b〔1〕,b〔2〕)a〔〕string{John,Paul,George,Ringo,Pete} 由于切片的零值nil用起来就像一个长度为零的切片,我们可以声明一个切片变量然后在循环中向它追加数据:Filterreturnsanewsliceholdingonlytheelementsofsthatsatisfyfn()funcFilter(s〔〕int,fnfunc(int)bool)〔〕int{varp〔〕intnilfor,v:ranges{iffn(v){pappend(p,v)}}returnp}可能的陷阱 正如前面所说,切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。有时候可能会因为一个小的内存引用导致保存所有的数据。 例如,FindDigits函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回。vardigitRegexpregexp。MustCompile(〔09〕)funcFindDigits(filenamestring)〔〕byte{b,:ioutil。ReadFile(filename)returndigitRegexp。Find(b)} 这段代码的行为和描述类似,返回的〔〕byte指向保存整个文件的数组。因为切片引用了原始的数组,导致GC不能释放数组的空间;只用到少数几个字节却导致整个文件的内容都一直保存在内存里。 要修复整个问题,可以将感兴趣的数据复制到一个新的切片中:funcCopyDigits(filenamestring)〔〕byte{b,:ioutil。ReadFile(filename)bdigitRegexp。Find(b)c:make(〔〕byte,len(b))copy(c,b)returnc} Rangepackagemainimportfmtfor循环的range形式可遍历切片或映射。当使用for循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。varpow〔〕int{1,2,4,8,16,32,64,128}funcmain(){fori,v:rangepow{fmt。Printf(2dd,i,v)}}packagemainimportfmt可以将下标或值赋予来忽略它。fori,:rangepowfor,value:rangepow若你只需要索引,忽略第二个变量即可。fori:rangepowfuncmain(){pow:make(〔〕int,10)fori:rangepow{pow〔i〕1uint(i)2i}for,value:rangepow{fmt。Printf(d,value)}} 映射packagemainimportfmt映射将键映射到值。映射的零值为nil。nil映射既没有键,也不能添加键。make函数会返回给定类型的映射,并将其初始化备用。typeVertexstruct{Lat,Longfloat64}varmmap〔string〕Vertexfuncmain(){mmake(map〔string〕Vertex)m〔BellLabs〕Vertex{40。68433,74。39967,}fmt。Println(m〔BellLabs〕)} 映射的文法packagemainimportfmt映射的文法与结构体相似,不过必须有键名typeVertexstruct{Lat,Longfloat64}varmmap〔string〕Vertex{BellLabs:Vertex{40。68433,74。39967,},Google:Vertex{37。42202,122。08408,},}funcmain(){fmt。Println(m)}packagemainimportfmt若顶级类型只是一个类型名,你可以在文法的元素中省略它typeVertexstruct{Lat,Longfloat64}varmmap〔string〕Vertex{BellLabs:{40。68433,74。39967},省略VertexGoogle:{37。42202,122。08408},}funcmain(){fmt。Println(m)} 修改映射packagemainimportfmt在映射m中插入或修改元素:m〔key〕elem获取元素:elemm〔key〕删除元素:delete(m,key)通过双赋值检测某个键是否存在:elem,okm〔key〕若key在m中,ok为true;否则,ok为false。若key不在映射中,那么elem是该映射元素类型的零值。同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。注:若elem或ok还未声明,你可以使用短变量声明:elem,ok:m〔key〕funcmain(){m:make(map〔string〕int)m〔Answer〕42fmt。Println(Thevalue:,m〔Answer〕)m〔Answer〕48fmt。Println(Thevalue:,m〔Answer〕)delete(m,Answer)fmt。Println(Thevalue:,m〔Answer〕)v,ok:m〔Answer〕fmt。Println(Thevalue:,v,Present?,ok)} 函数值packagemainimport(fmtmath)函数也是值。它们可以像其它值一样传递。函数值可以用作函数的参数或返回值。funccompute(fnfunc(float64,float64)float64)float64{returnfn(3,4)}funcmain(){hypot:func(x,yfloat64)float64{returnmath。Sqrt(xxyy)}fmt。Println(hypot(5,12))fmt。Println(compute(hypot))fmt。Println(compute(math。Pow))} 函数的闭包packagemainimportfmtGo函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量绑定在一起。例如,函数adder返回一个闭包。每个闭包都被绑定在其各自的sum变量上。funcadder()func(int)int{sum:0returnfunc(xint)int{sumxreturnsum}}funcmain(){pos,neg:adder(),adder()fori:0;i10;i{fmt。Println(pos(i),neg(2i),)}} 方法packagemainimport(fmtmath)Go没有类。不过你可以为结构体类型定义方法。方法就是一类带特殊的接收者参数的函数。方法接收者在它自己的参数列表内,位于func关键字和方法名之间。在此例中,Abs方法拥有一个名为v,类型为Vertex的接收者。typeVertexstruct{X,Yfloat64}func(vVertex)Abs()float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcmain(){v:Vertex{3,4}fmt。Println(v。Abs())} 方法即函数packagemainimport(fmtmath)记住:方法只是个带接收者参数的函数。现在这个Abs的写法就是个正常的函数,功能并没有什么变化。typeVertexstruct{X,Yfloat64}funcAbs(vVertex)float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcmain(){v:Vertex{3,4}fmt。Println(Abs(v))}packagemainimport(fmtmath)你也可以为非结构体类型声明方法。在此例中,我们看到了一个带Abs方法的数值类型MyFloat。你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括int之类的内建类型)的接收者声明方法。(译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)typeMyFloatfloat64func(fMyFloat)Abs()float64{iff0{returnfloat64(f)}returnfloat64(f)}funcmain(){f:MyFloat(math。Sqrt2)fmt。Println(f。Abs())} 指针接收者packagemainimport(fmtmath)你可以为指针接收者声明方法。这意味着对于某类型T,接收者的类型可以用T的文法。(此外,T不能是像int这样的指针。)例如,这里为Vertex定义了Scale方法。指针接收者的方法可以修改接收者指向的值(就像Scale在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。试着移除第16行Scale函数声明中的,观察此程序的行为如何变化。若使用值接收者,那么Scale方法会对原始Vertex值的副本进行操作。(对于函数的其它参数也是如此。)Scale方法必须用指针接受者来更改main函数中声明的Vertex的值。typeVertexstruct{X,Yfloat64}func(vVertex)Abs()float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}func(vVertex)Scale(ffloat64){v。Xv。Xfv。Yv。Yf}funcmain(){v:Vertex{3,4}v。Scale(10)fmt。Println(v。Abs())} 方法与指针重定向packagemainimportfmt比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:varvVertexScaleFunc(v,5)编译错误!ScaleFunc(v,5)OK而以指针为接收者的方法被调用时,接收者既能为值又能为指针:varvVertexv。Scale(5)OKp:vp。Scale(10)OK对于语句v。Scale(5),即便v是个值而非指针,带指针接收者的方法也能被直接调用。也就是说,由于Scale方法有一个指针接收者,为方便起见,Go会将语句v。Scale(5)解释为(v)。Scale(5)。typeVertexstruct{X,Yfloat64}func(vVertex)Scale(ffloat64){v。Xv。Xfv。Yv。Yf}funcScaleFunc(vVertex,ffloat64){v。Xv。Xfv。Yv。Yf}funcmain(){v:Vertex{3,4}v。Scale(2)ScaleFunc(v,10)p:Vertex{4,3}p。Scale(3)ScaleFunc(p,8)fmt。Println(v,p)}packagemainimport(fmtmath)同样的事情也发生在相反的方向。接受一个值作为参数的函数必须接受一个指定类型的值:varvVertexfmt。Println(AbsFunc(v))OKfmt。Println(AbsFunc(v))编译错误!而以值为接收者的方法被调用时,接收者既能为值又能为指针:varvVertexfmt。Println(v。Abs())OKp:vfmt。Println(p。Abs())OK这种情况下,方法调用p。Abs()会被解释为(p)。Abs()。typeVertexstruct{X,Yfloat64}func(vVertex)Abs()float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcAbsFunc(vVertex)float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcmain(){v:Vertex{3,4}fmt。Println(v。Abs())fmt。Println(AbsFunc(v))p:Vertex{4,3}fmt。Println(p。Abs())fmt。Println(AbsFunc(p))} 选择值或指针作为接收者packagemainimport(fmtmath)使用指针接收者的原因有二:首先,方法能够修改其接收者指向的值。其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。在本例中,Scale和Abs接收者的类型为Vertex,即便Abs并不需要修改其接收者。通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。typeVertexstruct{X,Yfloat64}func(vVertex)Scale(ffloat64){v。Xv。Xfv。Yv。Yf}func(vVertex)Abs()float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcmain(){v:Vertex{3,4}fmt。Printf(Beforescaling:v,Abs:v,v,v。Abs())v。Scale(5)fmt。Printf(Afterscaling:v,Abs:v,v,v。Abs())} 接口packagemainimport(fmtmath)接口类型是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。注意:示例代码存在一个错误。由于Abs方法只为Vertex(指针类型)定义,因此Vertex(值类型)并未实现Abser。typeAbserinterface{Abs()float64}typeMyFloatfloat64func(fMyFloat)Abs()float64{iff0{returnfloat64(f)}returnfloat64(f)}typeVertexstruct{X,Yfloat64}func(vVertex)Abs()float64{returnmath。Sqrt(v。Xv。Xv。Yv。Y)}funcmain(){varaAbserf:MyFloat(math。Sqrt2)v:Vertex{3,4}afaMyFloat实现了AbseravaVertex实现了Abser下面一行,v是一个Vertex(而不是Vertex)所以没有实现Abser。avfmt。Println(a。Abs())} 接口与隐式实现packagemainimportfmt类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有implements关键字。隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。typeIinterface{M()}typeTstruct{Sstring}此方法表示类型T实现了接口I,但我们无需显式声明此事。func(tT)M(){fmt。Println(t。S)}funcmain(){variIT{hello}i。M()} 接口值packagemainimport(fmtmath)接口也是值。它们可以像其它值一样传递。接口值可以用作函数的参数或返回值。在内部,接口值可以看做包含值和具体类型的元组:(value,type)接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。typeIinterface{M()}typeTstruct{Sstring}func(tT)M(){fmt。Println(t。S)}typeFfloat64func(fF)M(){fmt。Println(f)}funcmain(){variIiT{Hello}describe(i)i。M()iF(math。Pi)describe(i)i。M()}funcdescribe(iI){fmt。Printf((v,T),i,i)} 底层值为nil的接口值packagemainimportfmt即便接口内的具体值为nil,方法仍然会被nil接收者调用。在一些语言中,这会触发一个空指针异常,但在Go中通常会写一些方法来优雅地处理它(如本例中的M方法)。注意:保存了nil具体值的接口其自身并不为nil。typeIinterface{M()}typeTstruct{Sstring}func(tT)M(){iftnil{fmt。Println(nil)return}fmt。Println(t。S)}funcmain(){variIvartTitdescribe(i)i。M()iT{hello}describe(i)i。M()}funcdescribe(iI){fmt。Printf((v,T),i,i)} nil接口值packagemainimportfmtnil接口值既不保存值也不保存具体类型。为nil接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个具体方法的类型。typeIinterface{M()}funcmain(){variIdescribe(i)i。M()}funcdescribe(iI){fmt。Printf((v,T),i,i)}结果:(nil,nil)panic:runtimeerror:invalidmemoryaddressornilpointerdereference〔signalSIGSEGV:segmentationviolationcode0x1addr0x0pc0x47f4a7〕goroutine1〔running〕:main。main()tmpsandbox3961277646prog。go:120x67 空接口packagemainimportfmt指定了零个方法的接口值被称为空接口:interface{}空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)空接口被用来处理未知类型的值。例如,fmt。Print可接受类型为interface{}的任意数量的参数。funcmain(){variinterface{}describe(i)i42describe(i)ihellodescribe(i)}funcdescribe(iinterface{}){fmt。Printf((v,T),i,i)} 类型断言packagemainimportfmt类型断言提供了访问接口值底层具体值的方式。t:i。(T)该语句断言接口值i保存了具体类型T,并将其底层类型为T的值赋予变量t。若i并未保存T类型的值,该语句就会触发一个恐慌。为了判断一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。t,ok:i。(T)若i保存了一个T,那么t将会是其底层值,而ok为true。否则,ok将为false而t将为T类型的零值,程序并不会产生恐慌。请注意这种语法和读取一个映射时的相同之处。funcmain(){variinterface{}hellos:i。(string)fmt。Println(s)s,ok:i。(string)fmt。Println(s,ok)f,ok:i。(float64)fmt。Println(f,ok)fi。(float64)报错(panic)fmt。Println(f)} 类型选择packagemainimportfmt类型选择是一种按顺序从几个类型断言中选择分支的结构。类型选择与一般的switch语句相似,不过类型选择中的case为类型(而非值),它们针对给定接口值所存储的值的类型进行比较。switchv:i。(type){caseT:v的类型为TcaseS:v的类型为Sdefault:没有匹配,v与i的类型相同}类型选择中的声明与类型断言i。(T)的语法相同,只是具体类型T被替换成了关键字type。此选择语句判断接口值i保存的值类型是T还是S。在T或S的情况下,变量v会分别按T或S类型保存i拥有的值。在默认(即没有匹配)的情况下,变量v与i的接口类型和值相同。funcdo(iinterface{}){switchv:i。(type){caseint:fmt。Printf(Twicevisv,v,v2)casestring:fmt。Printf(qisvbyteslong,v,len(v))default:fmt。Printf(IdontknowabouttypeT!,v)}}funcmain(){do(21)do(hello)do(true)} Stringerpackagemainimportfmtfmt包中定义的Stringer是最普遍的接口之一。typeStringerinterface{String()string}Stringer是一个可以用字符串描述自己的类型。fmt包(还有很多包)都通过此接口来打印值。typePersonstruct{NamestringAgeint}func(pPerson)String()string{returnfmt。Sprintf(v(vyears),p。Name,p。Age)}funcmain(){a:Person{ArthurDent,42}z:Person{ZaphodBeeblebrox,9001}fmt。Println(a,z)} 错误packagemainimport(fmttime)Go程序使用error值来表示错误状态。与fmt。Stringer类似,error类型是一个内建接口:typeerrorinterface{Error()string}(与fmt。Stringer类似,fmt包在打印值时也会满足error。)通常函数会返回一个error值,调用的它的代码应当判断这个错误是否等于nil来进行错误处理。i,err:strconv。Atoi(42)iferr!nil{fmt。Printf(couldntconvertnumber:v,err)return}fmt。Println(Convertedinteger:,i)error为nil时表示成功;非nil的error表示失败。typeMyErrorstruct{Whentime。TimeWhatstring}func(eMyError)Error()string{returnfmt。Sprintf(atv,s,e。When,e。What)}funcrun()error{returnMyError{time。Now(),itdidntwork,}}funcmain(){iferr:run();err!nil{fmt。Println(err)}} Readerpackagemainimport(fmtiostrings)io包指定了io。Reader接口,它表示从数据流的末尾进行读取。Go标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。io。Reader接口有一个Read方法:func(T)Read(b〔〕byte)(nint,errerror)Read用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个io。EOF错误。示例代码创建了一个strings。Reader并以每次8字节的速度读取它的输出。funcmain(){r:strings。NewReader(Hello,Reader!)b:make(〔〕byte,8)for{n,err:r。Read(b)fmt。Printf(nverrvbv,n,err,b)fmt。Printf(b〔:n〕q,b〔:n〕)iferrio。EOF{break}}}结果:n8errnilb〔72101108108111443282〕b〔:n〕Hello,Rn6errnilb〔10197100101114333282〕b〔:n〕eader!n0errEOFb〔10197100101114333282〕b〔:n〕 Go程packagemainimport(fmttime)Go程(goroutine)是由Go运行时管理的轻量级线程。gof(x,y,z)会启动一个新的Go程并执行f(x,y,z)f,x,y和z的求值发生在当前的Go程中,而f的执行发生在新的Go程中。Go程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync包提供了这种能力,不过在Go中并不经常用到,因为还有其它的办法funcsay(sstring){fori:0;i5;i{time。Sleep(100time。Millisecond)fmt。Println(s)}}funcmain(){gosay(world)say(hello)} 信道packagemainimportfmt信道是带有类型的管道,你可以通过它用信道操作符来发送或者接收值。chv将v发送至信道ch。v:ch从ch接收值并赋予v。(箭头就是数据流的方向。)和映射与切片一样,信道在使用前必须创建:ch:make(chanint)默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得Go程可以在没有显式的锁或竞态变量的情况下进行同步。以下示例对切片中的数进行求和,将任务分配给两个Go程。一旦两个Go程完成了它们的计算,它就能算出最终的结果。funcsum(s〔〕int,cchanint){sum:0for,v:ranges{sumv}csum将和送入c}funcmain(){s:〔〕int{7,2,8,9,4,0}c:make(chanint)gosum(s〔:len(s)2〕,c)gosum(s〔len(s)2:〕,c)x,y:c,c从c中接收fmt。Println(x,y,xy)} 带缓冲的信道packagemainimportfmt信道可以是带缓冲的。将缓冲长度作为第二个参数提供给make来初始化一个带缓冲的信道:ch:make(chanint,100)仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。修改示例填满缓冲区,然后看看会发生什么。funcmain(){ch:make(chanint,2)ch1ch2fmt。Println(ch)fmt。Println(ch)} range和closepackagemainimport(fmt)发送者可通过close关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完v,ok:ch之后ok会被设置为false。循环fori:rangec会不断从信道接收值,直到它被关闭。注意:只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。还要注意:信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个range循环。funcfibonacci(nint,cchanint){x,y:0,1fori:0;in;i{cxx,yy,xy}close(c)}funcmain(){c:make(chanint,10)gofibonacci(cap(c),c)fori:rangec{fmt。Println(i)}} select语句packagemainimportfmtselect语句使一个Go程可以等待多个通信操作。select会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。funcfibonacci(c,quitchanint){x,y:0,1for{select{casecx:x,yy,xycasequit:fmt。Println(quit)return}}}funcmain(){c:make(chanint)quit:make(chanint)gofunc(){fori:0;i10;i{fmt。Println(c)}quit0}()fibonacci(c,quit)} 默认选择packagemainimport(fmttime)当select中的其它分支都没有准备好时,default分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用default分支:select{casei:c:使用idefault:从c中接收会阻塞时执行}funcmain(){tick:time。Tick(100time。Millisecond)boom:time。After(500time。Millisecond)for{select{casetick:fmt。Println(tick。)caseboom:fmt。Println(BOOM!)returndefault:fmt。Println(。)time。Sleep(50time。Millisecond)}}} sync。Mutexpackagemainimport(fmtsynctime)我们已经看到信道非常适合在各个Go程间进行通信。但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个Go程能够访问一个共享的变量,从而避免冲突?这里涉及的概念叫做互斥(mutualexclusion),我们通常使用互斥锁(Mutex)这一数据结构来提供这种机制。Go标准库中提供了sync。Mutex互斥锁类型及其两个方法:LockUnlock我们可以通过在代码前调用Lock方法,在代码后调用Unlock方法来保证一段代码的互斥执行。参见Inc方法。我们也可以用defer语句来保证互斥锁一定会被解锁。参见Value方法。SafeCounter的并发使用是安全的。typeSafeCounterstruct{vmap〔string〕intmuxsync。Mutex}Inc增加给定key的计数器的值。func(cSafeCounter)Inc(keystring){c。mux。Lock()Lock之后同一时刻只有一个goroutine能访问c。vc。v〔key〕c。mux。Unlock()}Value返回给定key的计数器的当前值。func(cSafeCounter)Value(keystring)int{c。mux。Lock()Lock之后同一时刻只有一个goroutine能访问c。vdeferc。mux。Unlock()returnc。v〔key〕}funcmain(){c:SafeCounter{v:make(map〔string〕int)}fori:0;i1000;i{goc。Inc(somekey)}time。Sleep(time。Second)fmt。Println(c。Value(somekey))}