zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Go语言中的复合类型详细介绍

Go语言 详细 类型 介绍 复合
2023-06-13 09:15:29 时间

golang复合类型包括:结构体、数组、切片、Maps。

1、数组

数组

golang中的数组与C语言中的数组差异很大,倒更类似Pascal中的数组。(Slice,下个话题,有些像C语言中的数组)

复制代码代码如下:


varar[3]int

声明ar为一个拥有三个整型数的数组,所有元素初始化为0。

大小是类型的一个组成部分。

内置的函数len可以用于获取数组大小:

复制代码代码如下:


len(ar)=3

数组是值类型

golang中的数组是值,而非C语言中的隐式指针。你可以获得数组的地址,并生成一个指向数组的指针(例如,将其高效地传递给函数):

复制代码代码如下:
funcf(a[3]int){fmt.Println(a)}  
funcfp(a*[3]int){fmt.Println(a)}  
 
funcmain(){  
   varar[3]int 
   f(ar)//传递一个ar的拷贝  
   fp(&ar)//传递一个指向ar的指针  

输出结果:

复制代码代码如下:
[000]

&[000]

数组字面值

所有的符合类型都有相同的值创建语法。以数组为例,其语法如下:

3个整数的数组:

复制代码代码如下:
[3]int{1,2,3}

10个整数的数组,前三个元素不是0:

复制代码代码如下:
[10]int{1,2,3}

不想数?使用…代表长度:

复制代码代码如下:
[...]int{1,2,3}

不想初始化所有值?使用key:value对:

复制代码代码如下:
[10]int{2:1,3:1,5:1,7:1}

指向数组字面值的指针

你可以获取数组字面值的地址,这样可以得到一个指向新建数组实例的指针:

复制代码代码如下:
funcfp(a*[3]int){fmt.Println(a)}  
funcmain(){  
   fori:=0;i<3;i++{  
       fp(&[3]int{i,i*i,i*i*i})  
   }  

输出结果:

复制代码代码如下:
&[000]
&[111]
&[248]

2、切片(Slice)

切片

切片是对数组中某一段的引用。

切片比普通数组应用得更多也更广泛。

切片使用的代价很低。

一个切片类型很像一个没有大小的数组类型:

复制代码代码如下:
vara[]int

内置的len(a)可以返回切片中元素的个数。

通过对数组或切片进行"切片",我们可以创建一个新切片:

复制代码代码如下:
a=ar[7:9]

a(上面例子中的a)的有效下标值是0和1;len(a)==2。

切片速记

当对数组进行切片时,第一个下标值默认是0:

ar[:n]等价于a[0:n]。

第二个下标值默认为len(array/slice):

ar[n:]等价于ar[n:len(ar)]。

因此由数组创建切片时:

ar[:]等价于ar[0:len(ar)]。

切片引用数组

概念上:

复制代码代码如下:
typeSlicestruct{
base*elemType//指向0th元素的指针
lenint//切片中元素的数量
capint//切片可以容纳元素的数量
}

数组:

复制代码代码如下:
ar:715438721153

切片:

复制代码代码如下:
a=ar[7:9]:base=&ar[7](指向ar中的2)len=2cap=4

创建切片

切片字面值看起来像没有指定大小的数组字面值:

复制代码代码如下:
varslice=[]int{1,2,3,4,5}

上面代码创建了一个长度为5的数组并创建一个切片用于引用这个数组。

我们可以使用内置的make函数分配一个切片(底层实际是个数组):

复制代码代码如下:
vars100=make([]int,100)//slice:100ints

为何用make而不是用new?因为我们需要创建切片,而不仅仅是为了分配内存。注意make([]int,10)返回[]int,而new([]int)返回*[]int。

使用make创建切片、map以及channel。

切片容量

切片是对底层数组的一个引用。因此存在一些在数组里但却没在切片引用的范围内的元素。

内置的函数cap(capacity)用于报告切片可能增长到多长。

复制代码代码如下:
varar=[10]int{0,1,2,3,4,5,6,7,8,9}
vara=ar[5:7]//引用子数组{5,6}

len(a)=2,cap(a)=5,现在我们可以重新切片:

复制代码代码如下:
a=a[0:4]//引用子数组{5,6,7,8}

len(a)现在是4,而cap(a)依旧是5。

调整切片大小

切片可被当作可增长的数组用。使用make分配一个切片,并指定其长度和容量。当要增长时,我们可以做重新切片:

复制代码代码如下:
varsl=make([]int,0,100)//长度0,容量100  
funcappendToSlice(iint,sl[]int)[]int{  
   iflen(sl)==cap(sl){error(…)}  
   n:=len(sl)  
   sl=sl[0:n+1]//长度增加1  
   sl[n]=i  
   returnsl  
}

因此,sl的长度总是元素的个数,但其容量可根据需要增加。

这种手法代价很小,并且是Go语言中的惯用法。

切片使用的代价很小

你可以根据需要自由地分配和调整切片大小。它们的传递仅需要很小的代价;不必分配。

记住它们是引用,因此下层的存储可以被修改。

例如,I/O使用切片,而不是计数:

复制代码代码如下:
funcRead(fdint,b[]byte)int 
varbuffer[100]byte  
   fori:=0;i<100;i++{  
   //每次向Buffer中填充一个字节  
   Read(fd,buffer[i:i+1])//noallocationhere  

拆分一个Buffer:

复制代码代码如下:
header,data:=buf[:n],buf[n:]

字符串也可以被切片,而且效率相似。

3、Maps

maps

Map是另外一种引用类型。它们是这样声明的:

复制代码代码如下:
varmmap[string]float64

这里声明了一个map,索引key的类型为string,值类型为float64。这类似于C++中的类型*map<string,float64>。

对于给定mapm,len(m)返回key的数量。

map的创建

和创建一个切片一样,一个map变量是一个空引用;在可以使用它之前,应先要向里面放入一些内容。

三种方式:

1)字面值:逗号分隔的key:value对列表

复制代码代码如下:
m=map[string]float64{"1":1,"pi":3.1415}

2)创建
复制代码代码如下:
m=make(map[string]float64)//makenotnew

3)赋值
复制代码代码如下:
varm1map[string]float64
m1=m//m1和m现在引用相同的map

map索引

(接下来的几个例子全都使用:

复制代码代码如下:
m=map[string]float64{"1":1,"pi":3.1415})

访问一个元素;如果该元素不存在,则得到对应mapvalue类型的零值:
复制代码代码如下:
one:=m["1"]
zero:=m["notpresent"]//zero被置为0.0.

设置一个元素的值(两次设置将更新为最新值)
复制代码代码如下:
m["2"]=2
m["2"]=3//思维混乱

测试存在性

要测试一个map中是否存在某个key,我们可以使用一个多项赋值的"comma,om"形式:

复制代码代码如下:
m=map[string]float64{"1":1,"pi":3.1415}

varvaluefloat64
varpresentbool

value,present=m[x]

或者按惯例:

复制代码代码如下:
value,ok:=m[x]//"commaok"形式

如果map中存在x这个key,布尔变量会被设置为true;value会被赋值为map中key对应的值。相反,布尔变量会被设置为false,value被设置为相应值类型的零值。

删除

使用多元赋值可以删除map中的一个值:

复制代码代码如下:
m=map[string]float64{"1":1.0,"pi":3.1415}

varkeepbool
varvaluefloat64
varxstring=f()

m[x]=v,keep

如果keep的值为true,则将v赋值到map中;如果keep为false,则删除map中的keyx。因此删除一个key:

复制代码代码如下:
m[x]=0,false//从map中删除x

译注:Go1中上述的删除方式已被取消,取而代之的是delete(m,x)。

for和range

对于数组、切片和map(以及我们在第三部分将要看到的更多类型),for循环提供了一种特殊的语法用于迭代访问其中的元素。

复制代码代码如下:
m:=map[string]float64{"1":1.0,"pi":3.1415}

forkey,value:=rangem{
fmt.Printf("key%s,value%g\n",key,value)
}

只用一个变量,我们可以获得key:

复制代码代码如下:
forkey=rangem{
fmt.Printf("key%s\n",key)
}

变量可以用:=赋值或声明。

对于数组和切片来说,通过这种方式我们可以获得元素的下标以及元素值。

将range用于字符串

将forrange用于字符串时,实际迭代的元素是Unicode码点(codepoint),而不是字节(对字节,可使用[]byte或使用标准的for语句)。我们假设字符串包

含使用UTF-8编码的字符。

下面循环:

复制代码代码如下:
s:="[\u00ff\u754c]"
fori,c:=ranges{
fmt.Printf("%d:%q",i,c)//%qfor"quoted"
}

输出:0:"["1:"ÿ"3:"界"6:"]"

如果遇到了错误的UTF-8码点,这个字符将被设置为U+FFFD,下标向后移动一个字节。

4、Structs

structs

对于Go中的struct,你应该感觉十分熟悉:简单的数据字段声明。

复制代码代码如下:
varpstruct{
x,yfloat64
}

更常用的是:

复制代码代码如下:

typePointstruct{
x,yfloat64
}
varpPoint

struct允许程序员定义内存布局。

struct是值类型

struct是值类型,new(StructType)返回一个指向零值的指针(分配的内存都被置0)。

复制代码代码如下:
typePointstruct{
x,yfloat64
}
varpPoint
p.x=7
p.y=23.4
varpp*Point=new(Point)
*pp=p
pp.x=Pi//(*pp).x的语法糖

对于结构体指针,没有->符号可用。Go提供了间接的方式。

创建结构体

结构体是值类型,因此你可只通过声明就可以创建一个全0的结构体变量。

你也可以使用new创建一个结构体。

复制代码代码如下:
varpPoint//零值
pp:=new(Point)//惯用法

结构体字面值语法也不出所料:

复制代码代码如下:
p=Point{7.2,8.4}
p=Point{y:8.4,x:7.2}
pp=&Point{7.2,8.4}//惯用法
pp=&Point{}//也是惯用法,==new(Point)

和数组一样,得到了结构体字面值的地址,就得到了新建结构体的地址。

这些例子都是构造器。

导出类型和字段

只有当结构体的字段(和方法,即将讲解)名字的首字母大写时,它才能被包外可见。

私有类型和字段:

复制代码代码如下:
typepointstruct{x,yfloat64}

导出类型和字段:
复制代码代码如下:
typePointstruct{X,Yfloat64}

导出类型和私有类型混合字段:
复制代码代码如下:
typePointstruct{
X,Yfloat64//exported
namestring//notexported
}

你甚至可以创建一个带有导出字段的私有类型。(练习:何时能派上用场呢?)

匿名字段

在一个结构体内,你可以声明不带名字的字段,比如另外一个结构体类型。这些字段被称为匿名字段。它们看起来就像里层的结构体简单插入或“嵌入”到

外层结构体似的。

这个简单的机制为从其他类型继承已有的实现提供了一种方法。

下面是一个例子。

一个匿名结构体字段:

复制代码代码如下:
typeAstruct{
ax,ayint
}

typeBstruct{
A
bx,byfloat64
}

B看起来像有四个字段ax、ay、bx和by。B可看成{ax,ayint;bx,byfloat64}。

然后B的字面值必须提供细节:

复制代码代码如下:
b:=B{A{1,2},3.0,4.0}
fmt.Println(b.ax,b.ay,b.bx,b.by)

输出1234

匿名字段以类型作为名字

匿名字段不仅仅是简单插入这些字段这么简单,其含义更为丰富:B还拥有字段A。匿名字段看起来就像名字为其类型名的字段。

复制代码代码如下:
b:=B{A{1,2},3.0,4.0}
fmt.Println(b.A)

输出:{12}。如果A来自于另外一个包,这个字段依旧被称为A。

复制代码代码如下:
import"pkg"
typeCstruct{pkg.A}

c:=C{pkg.A{1,2}}
fmt.Println(c.A)//不是c.pkg.A

任意类型的匿名字段

任何具名类型或指向具名类型的指针都可以用作匿名字段。它们可以出现在结构体中的任意位置。

复制代码代码如下:
typeCstruct{
xfloat64
int
string
}
c:=C{3.5,7,"hello"}
fmt.Println(c.x,c.int,c.string)

输出:3.57hello

冲突和遮蔽

如果有两个字段具有相同的名字(可能是一个继承类型的名字),代码将遵循下面规则:

1)外层的名字遮蔽内层的名字。这提供了一个重写字段/方法的方式。
2)如果在同一层次上出现了相同的名字,如果名字被使用,那么将是一个错误。(如果没有使用,不会出现错误)

二义性是没有规则能解决的,必须被修正。

冲突的例子

复制代码代码如下:
typeAstruct{aint}
typeBstruct{a,bint}
typeCstruct{A;B}
varcC

使用c.a将会出现错误。它到底是c.A.a还是c.B.a呢?

复制代码代码如下:
typeDstruct{B;bfloat64}
vardD

使用d.b没有问题:它是float64类型变量,不是d.B.b。要获得内层的b,可用d.B.b。