神刀安全网

深入Go语言 – 3

本文介绍Go的类型系统,以及类型的比较和语句块。

Go语言包含11种类型,你应该很熟悉了,下面让我们再深入的了解一下每种类型的细节。

  • 布尔类型
  • 数值类型
  • 字符串类型
  • 数组类型
  • Slice类型
  • Struct类型
  • 指针类型
  • 函数类型
  • 接口类型
  • Map类型
  • Channel类型

bool、数值型类型、rune、字符串都是预定义的类型:

boolbytecomplex64complex128errorfloat32float64 intint8int16int32int64runestring uintuint8uint16uint32uint64uintptr 

复合类型array, struct, pointer, function, interface, slice, map 和 channel 由类型字面量构造而成。

每一个类型都有一个底层类型(underlying type),比如下面的代码:

typeT1string typeT2 T1 typeT3 []T1 typeT4 T3 

stringT1T2 的底层类型

  • 命名类型(named type) : 由一个确定的类型的名称指定
  • 未命名类型(unamed type) :由类型字面量指定,类型字面量由既有的类型组成

比如下面的例子中, x的类型就是未命名类型,

varxstruct{ Iint} 

而下面的例子中,y的类型就是命名类型。

typeFoostruct{ Iint} vary Foo 

未命名类型的一个重要属性就是用同样类型的未命名类型声明的变量拥有相同的类型,而两个不同的命名类型,即使底层的类型相同,它们的类型也是不同的。更详细的总结会在下一篇文章中介绍,比如我们再定义两个类型:

varx2struct{ Iint} typeBarstruct{ Iint} vary2 Bar 

其中 xx2 的类型相同, 而 yy2 的类型却不相同。

命名类型可以定义自己的函数, 而未命名类型确不行。

func(f *Foo) Hello() { }  func(a []int) Hello() {//错误, []int是未命名类型  } 

比如上例中, []int 是未命名类型,没办法为它定义方法,所以我们可以像下面的例子一样定义一个命名类型,它的底层类型是 []int :

typeIntArr []int  func(ia *IntArr) Hello() {  } 

参考

布尔类型

很简单,类型名为 bool ,只有两个值: truefalse

参考

数值类型

数值类型包括整数、浮点数和复数。总结如下:

类型 说明
uint8 无符号8位整数,(0 to 255)|
uint16 无符号16位整数, (0 to 65535)
uint32 无符号32位整数, (0 to 4294967295)
uint64 无符号64位整数, (0 to 18446744073709551615)
int8 8位整数, (-128 to 127)
int16 16位整数, (-32768 to 32767)
int32 16位整数, (-2147483648 to 2147483647)
int64 64位整数, (-9223372036854775808 to 9223372036854775807)
float32 IEEE-754 32位浮点数
float64 IEEE-754 64位浮点数
complex64 实部和虚部都是float32
complex128 实部和虚部都是float32
byte uint8的别名
rune int32的别名

在不同的架构上,下面的几种类型可能的位数不同:
uint 可能是32位或者64位
int 可能是32位或者64位
uintptr 是一个足够大的无符号整数,可以代表一个指针的值得

intint32 并不是一个相同的类型,尽管在一些环境下它们的位数都是32位。

数组类型

数组代表有限的同一元素类型的对象的序列。对象的数量就是数组的长度, 通过 len 方法得到。

数组是一维的,但是你可以构造多维数组:

[3][5]int [2][2][2]float64// 等价 [2]([2]([2]float64)) 

数组可以通过下面的方式声明,需要指定它的长度:

varbuffer[256]byte 

声明和初始化可以合在一起:

buffer := [10]string{}// len(buffer) == 10 intSet := [6]int{1,2,3,5}// len(intSet) == 6 days := [...]string{"Sat","Sun"}// len(days) == 2 

[...] 是根据初始化的元素的数量确定数组的长度。

Slice类型

实际在开发的过程中,我们使用数组的场合比较少,这是因为数组一旦定义,它的长度就不能再发生变化,这和很多其它编程语言的定义是一样的。更多的情况下我们会使用slice。

Slice描述了数组的一个连续的片段。

varslice []byte= buffer[100:150] 

上面的例子中 slice 对象代表数组的索引位置为100 ~149的元素。

其实slice数据结构是由 SliceHeader 描述的,所以我们可以看到slice是由三个数据描述的: 第0个元素的指针长度容量

typeSliceHeaderstruct{  Data uintptr  Len int  Cap int } 

在后面的文章中我会介绍通过指针操作SliceHeader。

slice可以从数组中生成,如上面的例子,也可以直接生成:

vars1 []int vars2 =make([]int,10) vars3 =make([]int,10,20)  fmt.Printf("len:%d, cap:%d/n",len(s1),cap(s1)) fmt.Printf("len:%d, cap:%d/n",len(s2),cap(s2)) fmt.Printf("len:%d, cap:%d/n",len(s3),cap(s3)) 

在生成的时候可以指定slice的长度和容量,如果没有指定容量(make的第三个参数),则容量和长度相同。
如果不是通过 make 创建,而是通过 var 的方式声明一个零值的slice,则它的长度和容量都为0。

一旦一个slice创建出来,它的底层元素是由一个数组保存着。slice的容量就是这个数组的长度,可以通过 cap 获得。
如果slice的元素的数量超过容量,就需要创建新的数组。这是一个值得注意的地方,如果你初始的时候就可以确定元素的最大数量的情况下,
最好设置slice的容量的值,这样避免数组的重新分配和数据拷贝,提高程序的性能。

index, append, remove 和 copy

slice和数组一样,都是可以通过索引得到某个位置的元素。

a := [...]int{1,2,3,4,5} vars = a[:]  fmt.Printf("%d/n", s[0]) fmt.Printf("%d/n", s[10])//panic: runtime error: index out of range 

如果超出slice的索引最大值,就会导致panic。

可以往slice增加元素:

s2 := append(s,6) fmt.Printf("s: %#v/ns2:%#v/n", s, s2)  //output: //s: []int{1, 2, 3, 4, 5} //s2:[]int{1, 2, 3, 4, 5, 6} 

通过 append 方法可以往slice增加元素,值得注意的是append的返回值是增加元素后的slice,和原始的slice不同,尽管它们底层的数组可能相同。
如果容量足够,元素就继续增加底层数组中,如果容量不够,则结果slice就会创建新的数组。

funcappend(slice []Type, elems ...Type) []Type 

你可以往slice一次增加多个元素:

slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...) 

注意 ... 写法,它意味着你可以把一个slice中所有的元素全部增加到另外一个slice的尾部:

s3 := append(s, s...) fmt.Printf("s: %#v/ns3:%#v/n", s, s3) 

一个值得注意的技巧是可以将一个字符串的byte一次都增加到一个 []byte中:

s4 := append([]byte("hello "),"world"...) 

要删除slice某个索引的位置,可以通过下面的方法:

s5 := append(s[:2], s[3:]...) fmt.Printf("s: %#v/ns5:%#v/n", s, s5) 

这个例子删除索引2处的元素。

slice的拷贝是通过 copy 方法。

funccopy(dst, src []Type)int 

返回结果为 dst和src的长度的最小值。这也就是说,如果dst的长度大,则将src全部元素都复制到dst中。如果src的长度大,则将src的len(dst)个元素复制到dst中。

slice类型再深入

因为slice底层使用数组,而这个数组可能在数组和多个slice中功能,这会带来潜在的问题。

1、对数组中元素的更改会影响slice下面的例子中我们将数组的第一个值改为100,可以看到slice的第一个值也变了。

a := [...]int{1,2,3,4,5} vars = a[:]  a[0] =100 fmt.Printf("a: %#v/ns:%#v/n", a, s) 

2、如果slice使用不同的数组,则不会有影响
这一条是显而易见的,既然数组都不相同了,当然也没有什么影响了。
但是有时候你不是很容易的发现:

a := [...]int{1,2,3,4,5} vars = a[:] s = append(s,6)  a[0] =100 fmt.Printf("a: %#v/ns:%#v/n", a, s) 

第三行在增加6到数组的尾部的时候,其实slice的容量已经不够了,所以为返回结果的slice新建了数组。因此对原始数组的更改不会影响s2,但是对s却有影响,s的第一个值也变了。

3、当两个slice有重叠时,可能会有影响

a := [...]int{1,2,3,4,5} s1 := a[0:1] s2 := a[0:2]  s1 = append(s1,100)  fmt.Printf("a: %#v/ns1:%#v/ns2:%#v/n", a, s1, s2) 

这个例子s1和s2都使用相同的数组a,而且它们的容量都是5,不同的是它们的长度分别是2和3。

当往s1增加增加一个元素的时候,它事实上将元素放在的数组的索引为2的位置。这会对原始数组和s2都有影响,看到检查输出的结果:

a:[5]int{1, 100, 3, 4, 5} s1:[]int{1, 100} s2:[]int{1, 100} 

4、 copy的时候有重叠
调用 copy 方法的时候也可能会产生副作用,比如下面的例子,copy到s1的操作导致底层的数组改变了,影响了a,s1,s2的值

a := [...]int{1,2,3,4,5} s1 := a[0:3] s2 := a[1:5] copy(s1, s2)  fmt.Printf("a: %#v/ns1:%#v/ns2:%#v/n", a, s1, s2) 

参考

字符串类型

字符串类型(string)是一个常用的类型,它的值是一组基本按照UTF-8编码的字节序列,可以为空。在前一篇文章中我已经介绍了字符串类型,所以此处不再重复介绍了,我会介绍一些有趣的性能。

字符串是不可变的,一旦创建,它的值就不能修改了。

内建的 len 方法可以得到字符串的长度,每个字节可以根据索引得到,不能像 C 语言一样得到某个字节的地址, &s[i] 这样做是非法的。

你可以把字符串看成一个不可变的slice,比如根据索引得到某个位置的字节,copy操作, append 字符串到[]byte中等:

s := "hello world" b := make([]byte,5) copy(b, s) fmt.Println(s[0]) fmt.Println(b) fmt.Println(append([]byte{}, s...)) 

字符串和slices of bytes可以很方便的进行互转,因为它们的结构类似,底层都是通过一个数组保存元素的:

s := "hello world" b := []byte(s) s = string(b) 

通常情况下,这种转换是对底层数组的复制,所以对转换后的slice的更改不会影响原来的字符串,这也保证了字符串的不可变。

但是,数组的复制是有代价的,内存的分配和数据的拷贝以及垃圾回收都会带来性能等开销,所以在追求性能的场合,比如一些Web框架中,采用来了一些"花招"实现"零拷贝"。

首先我们看看stirng和slice的数据结构是怎么样的:

typeSliceHeaderstruct{  Data uintptr  Len int  Cap int }  typeStringHeaderstruct{  Data uintptr  Len int } 

看起来结构类似明知不过slice对string多了一个Cap字段。所以我们可以根据它们的结构进行转换,不需要拷贝底层的数据Data:

import( "fmt" "reflect" "unsafe" )  funcmain() {  s := "hello world"  b := StringToBytes(s)  s = BytesToString(b)   fmt.Printf("%s/n", s) }  funcBytesToString(b []byte)string{  bytesHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))  strHeader := reflect.StringHeader{bytesHeader.Data, bytesHeader.Len} return*(*string)(unsafe.Pointer(&strHeader)) }  funcStringToBytes(sstring) []byte{  strHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))  bytesHeader := reflect.SliceHeader{strHeader.Data, strHeader.Len, strHeader.Len} return*(*[]byte)(unsafe.Pointer(&bytesHeader)) } 

整数之间、整数和字符串之间、slice和整数之间的转换放在数据转换一文中专门介绍。参考

Struct类型

了解C语言的同学都知道struct(结构体)。

struct是一组命名的元素的序列,每个元素都有名字和类型,这些元素叫做结构体的字段(field)。字段名可能显示地指定,也可能隐式地指定。字段名不能重复。

struct类型有很多有趣的特性。

1、匿名字段空字段、占位字段自不必说,你应该都已经了解了:

typeAstruct{} typeBstruct{  i1 uint16  _ int16  i2 int } 

struct还允许定义匿名字段。 匿名字段️指指声明了类型的字段,也叫嵌入字段或者嵌入类型。嵌入类型由类型名T或者指向非接口类型的指针*T指定。T本身不能再是指针类型。 类型名作为字段的名字。

下面的例子中的结构体包含四个匿名字段,可以看到包名不会作为字段名的一部分。

struct{  T1 // 字段名 T1  *T2 // 字段名 T2  P.T3 // 字段名 T3  *P.T4 // 字段名 T4  x, y int// 字段名 x 和 y } 

下面的结构体的定义是非法的,因为三个字段重名了。

struct{  T   *T   *P.T  } 

如果结构体x有一个嵌入字段E,并且E拥有字段或者方法f,那么你可以直接调用x.f (如果x.f是一个合法的selector的话),这叫做字段提升(promoted)。

提升的字段就像结构体的正常的字段一样,除了初始化的时候不能像普通的字段设置。

假设有一个struct S和一个类型T, 提升的方法有以下的特性:

  • 如果 S 包含一个嵌入字段 T , 那么 S*S 的方法集包含 receiver T 的方法. 而 *S 还包含 receiver *T 的方法.
  • 如果 S 包含一个嵌入字段 *T , 那么 S*S 的方法集都包含 receiver T*T 的方法.

*S 可以直接访问 S 的方法,而不必求值后再访问。

关于方法集和receiver我们在以后再讲。

字段的声明中还可以包含一个缺省的字符串tag, 用来作为这个字段的属性,通过反射可以得到这个tag的值,在类型比较的时候会进行比较。经常用在结构体的序列化反序列化中,序列化库可以根据这些tag将相应的字段转换成合适的值。

typeColorGroupstruct{  Id int`json:"id" xml:"id,attr" msg:"id"`  Name string`json:"name" xml:"name" msg:"name"`  Colors []string`json:"colors" xml:"colors" msg:"colors"` } 

struct本身也可以是匿名的:

vara =struct{  Name string }{"鸟窝"} 

但是不能把一个匿名struct作为匿名字段,因为Go不知道如何命名此字段:

所以必须为匿名struct字段命名,初始化的时候还挺麻烦:

varu =struct{  Age int  Name struct{  FirstName string  LastName string  } }{Age:18, Name:struct{  FirstName string  LastName string }{"鸟","窝"}} 

指针类型

指针类型相对于于C语言,是一个简化版的指针,避免了C语言指针复杂的计算带来的陷阱。

指针类型就是在原有的类型前面加星号 *

假设有个操作数x, 类型为T, 那么 &T 则为x的指针,类型为 *T

你可以声明指向指针的指针:

x :=10 p := &x varpp **int= &p fmt.Printf("%d/n", **pp) 

但是不能像C语言一样直接移动指针:

x := [...]int{1,2,3,4,5} p := &x p = p +1 fmt.Printf("%d/n", *p) 

虽然我们不能直接移动指针,但是我们可以通过曲折的方法操作:

x := [...]int{1,2,3,4,5} p := &x[0]  //p = p + 1 index2Pointer := unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(x[0])) p = (*int)(index2Pointer)//x[1] fmt.Printf("%d/n", *p)//2 

这个例子中我们成功地将指针移动到第二个索引处,虽然它和 &x[1]的功能是一样的,但却表明我们可以根据偏移量计算指针。这种方法更多的应用到struct的字段值的读取中,一些序列化的库通过它来读取struct字段的值。

首先我们认识两个对象: uintptrunsafe.Pointer
uintptr 是一个足够大的整数,用来存放指针的位。

unsafe.Pointer 定义如下:

typeArbitraryTypeint// shorthand for an arbitrary Go type; it is not a real type typePointer *ArbitraryType 

Pointer代表指向任意类型的指针,它有四个独有的操作:
1) 任意类型的指针可以被转换成一个 Pointer对象.
2) 相反一个Pointer也可以转换成任意类型的指针.
3) 一个uintptr可以转换成一个Pointer.
4) 相反一个Pointer可以转换成uintptr.

通过Pointer我们就可以直接读取内存,使用起来要格外小心。
下面列出了几种转换:
1、*T -> Pointer to T2

variint64=100 varp *int64= &i//*int64 P := unsafe.Pointer(p) 

2、Pointer to T2 -> *T

variint64=100 varp *int64= &i//*int64  P := unsafe.Pointer(p)  p2 := (*int32)(P)//*int32 fmt.Println(*p2) 

3、Pointer -> uintptr

variint64=200<<32+100 varp = &i P0 := unsafe.Pointer(p)  P := unsafe.Pointer(uintptr(P0) +4) p2 := (*int32)(P)  fmt.Println(*p2) //200 

4、uintptr -> Pointer同上, Pointer(uintptr)转换即可。

内建的 new 函数可以为类型T创建零值的对象,它返回的对象类型为 *T

vari =new(int) vars =new(string) varj =new(struct{int})  fmt.Printf("%T %T %T/n", i, s, j)//*int *string *struct { int } 

参考

函数类型

在Go语言中,函数是第一类的,可以赋值给变量,当做参数传入传出。

参数名和返回值名可以省略,但是如果要省略,必须所有的参数名或者返回值名全省略,部分省略是不行的。

通过 ... 支持变参,但是变参只能作为最后一个参数。

如果不需要返回值,则不需要定义返回类型。

可以命名函数类型。

以下都是合法的函数类型:

func() func(xint)int func(a, _int, zfloat32)bool func(a, bint, zfloat32) (bool) func(prefixstring, values ...int) func(a, bint, zfloat64, opt ...interface{}) (successbool) func(int,int,float64) (float64, *[]int) func(nint)func(p *T)  typeHandlerfunc(ResponseWriter, *Request) 

注意此处介绍的是函数类型,函数的定义可以指定函数名称,也可以定义匿名函数。

接口类型

Go并没有Java那样的完全的面向对象的类型系统, 而是通过接口和duck typing支持面向对象的编程,也就是会呱呱叫的我们都认为它是鸭子。

一个接口代表一组方法的集合。任何实现了这些方法的类型的值都可以赋值给这个接口变量,我们也可以说这些类型实现了这个接口。

特殊的, interface{} 代表一个万能的接口,其它类型都实现了这个接口,有点像Java中的Object类。

接口也可以嵌入,只要保证方法名唯一即可:

typeReadWriterinterface{  Read(b Buffer) bool  Write(b Buffer) bool }  typeFileinterface{  ReadWriter // same as adding the methods of ReadWriter  Locker // same as adding the methods of Locker  Close() }  typeLockedFileinterface{  Locker  File // illegal: Lock, Unlock not unique  Lock() // illegal: Lock not unique } 

但是接口不能嵌入它本身,或者递归地嵌入本身。

参考

Map类型

Map类型在Go语言中提高到语言规范的级别,在Java只是作为类库中的类实现的。

Map是一组无序的键值对的集合。键的类型相同,值的类型也相同。

Map的变量的定义可以通过下面几种方式,中括号中是键的类型,后边接着是值的类型,键:

m1 := make(map[string]int) m2 := make(map[string]int,100) m3 := new(map[string]int) varm4map[string]int 

一般通过`make`方法生成,可以设置map的初始容量,但是在增加元素的时候容量会快速增加,这也是一般map集合应对元素扩展的时候的方法。

注意, map的键类型是必须是可以比较的(comparable),比如下面的类型都不能作为键值:

typeFuncfunc() typeSL []int typeMmap[int]int  funcmain() {  m1 := make(map[Func]int)//错误  m2 := make(map[SL]int)//错误  m3 := make(map[M]int)//错误 } 

只有以下类型可以比较,也就是可以应用比较操作符(>、 <、== 等):

  • 布尔型
  • 整型
  • 浮点型
  • 复数
  • 字符串
  • 指针
  • Channel
  • 接口
  • struct
  • 数组

参考

Channel类型

Channel类型我在另外一篇文章中专门介绍了,本文中就不再赘述:Go Channel 详解

类型的零值

当一个值被创建的时候,无论你是用什么方法,声明或者new方法创建,如果没有显示地初始化,Go就会给它分配一个零值(zero value)。

  • 布尔型: false
  • 整数: 0
  • 浮点数: 0.0
  • 字符串: ""
  • 指针: nil
  • 函数: nil
  • 接口: nil
  • slice: nil
  • channel: nil
  • map: nil

值得注意的是字符串,从Java转过来的程序员会以为字符串的零值是nil,其实不是,字符串的零值是空的字符串。

声明一下, 本系列的第一部分基本是按照Go语言的规范编写的,大量参考了Go语言规范的内容

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 深入Go语言 – 3

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址