Go语言学习-数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

基础类型

整型

不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型,即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。事实上,内置的len函数返回一个有符号的int。

var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}

​当使用fmt包打印一个数值时,我们可以用%d、%o或%x参数控制输出的进制格式。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的[1]副词告诉Printf函数再次使用第一个操作数。第二,%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。

o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

浮点型

浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,大约是 3.4e38;对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。

通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大(译注:因为float32的有效bit位只有23个,其它的bit位用于指数和符号;当整数大于23bit能表达的范围时,float32的表示将出现误差)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分。

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试:正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果;还有NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1)。函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值,nan := math.NaN()

var f float64 = 212
fmt.Println((f - 32) * 5 / 9)     // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32))     // "0";   5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float

复数

Go语言提供了两种精度的复数类型:complex64 和 complex128,分别对应 float32 和 float64 两种浮点数精度。内置的 complex 函数用于构建复数,内建的 real 和 imag 函数分别返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

​在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:

x := 1 + 2i
y := 3 + 4i

复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。


## 布尔型

如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

s != "" && s[0] == 'x'

其中s[0]操作如果应用于空字符串将会导致panic异常。
布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换。

字符串

一个字符串是一个不可改变的字节序列(字符串变量的值是可以变的),内置的len函数可以返回一个字符串中的字节数目。索引操作 s[i] 返回第 i 个字节的字节值,i 必须满足0 ≤ i < len(s)条件约束。如果试图访问超出字符串索引范围的字节将会导致panic异常。第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。所以就不能直接用长度和下标来一一访问,得用 range,range 能分出实际对应的每一个字符。如果输出的是编码的数字的话,可以用 string© 来输出实际对应的字符

+操作符将两个字符串连接构造一个新字符串。字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。可以直接用字符串某个范围内的判等来判断前后缀和子串。

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:(相当于是只读的)

s[0] = 'L' // compile error: cannot assign to s[0]

strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。

字符串和字节slice之间可以相互转换:

s := "abc"
b := []byte(s)
s2 := string(b)

从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。

bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的,但是随着string、byte或[]byte等类型数据的写入可以动态增长,一个bytes.Buffer变量并不需要初始化,因为零值也是有效的。

类型转换:( fmt.Sprintf可以处理格式化输出,就是换成另一个指定的格式)

	x := 123
	s := fmt.Sprintf("x=%b", x) // "x=1111011"

常量

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

一个常量的声明也可以包含一个类型和一个值,但是如果没有显式指明类型,那么将从右边的表达式推断类型。

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof。

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

周日将对应0,周一为1,如此等等。

const (
    FlagUp Flags = 1 << iota // is up
    FlagBroadcast            // supports broadcast access capability
    FlagLoopback             // is a loopback interface
    FlagPointToPoint         // belongs to a point-to-point link
    FlagMulticast            // supports multicast access capability
)

随着iota的递增,每个常量对应表达式1 << iota,是连续的2的幂,分别对应一个bit位置。

反正就是在这个常量组里,iota的初始值是0,接下来每次增加1,赋给常量的值是用当前的iota的值根据第一个计算式来进行计算。

复合数据类型

数组

长度固定(只要定义的时候在 [ ] 里写了长度,就是数组,长度就固定了)

初始化的时候可以指定某一个下标处的值:x := [5]int{2: 3}

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

算符!=遵循同样的规则。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

把数组传入函数的时候,要传数组的指针,这样才能改到外面的数。数组的长度是固定的,就是这个函数也只能对应长度为5的数组。

func temp(tmp *[5]int) {
	tmp[0] = 100
}

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的函数并不能接收指向 [6]int 类型数组的指针,而且也没有任何添加或删除数组元素的方法。由于这些原因,除了像SHA256这类需要处理特定大小数组的特例外,数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。

slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度。

一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量,即 len(x)cap(x)

下面的x,就是个slice,类型是 []int,长度和容量都是5。就是没有直接指明序列的长度。会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

x := []int{1, 2, 3, 4, 5}

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。(用 make 就是把相应需要的空间创建出来了)

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。


多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

​slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。就是新的和旧的的重叠部分其实是同一部分,底层都是共享之前的底层数组,因此这种操作都是常量时间复杂度。如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大。

	x := [5]int{1, 2, 3, 4, 5}
	y := x[1:] 	//2,3,4,5
	y[2] = 100 	//y:2,3,100,5,x:1,2,3,100,5

在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。但是因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。也就是往函数里传slice的话是不需要限定长度的,不用传指针,直接传值的话也能更新到原slice上。

func tmp(x []int) {
	x[0] = 100
}
	y := x[1:]
	tmp(y)
	fmt.Println(y)

//y:[100 3 4 5]
//x:[1 100 3 4 5]

slice 之间不能比较。一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0。如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。

内置的append函数用于向slice追加元素。x 和 y 在底层是用的同样的空间。因为是让 y 等于 append 的结果,append 相当于是返回在原本的基础上变化后的一个切片,原本的是在底层上变化的,切片变量的长度什么的是没有直接变化的。x 相当于长度没有变化,但实际在底层的第 4 个位置上存的就是10。显示出什么要看对应的长度实际是什么,有没有变化。

append 里是另外开一个slice。先处理长度,获取原本slice的长度len,让 len + 1,如果加完小于 cap,就直接让新 slice 为原本slice的相应部分,然后再把新位置的值赋上去,底层就还是一样的加完大于 cap,就要扩充cap,用make给新slice赋相应的len和cap,还得把原先的值复制过来,再把新位置的值赋上去。有扩充cap的话,就是另外开的空间,新的slice的底层就跟旧slice的不一样了。

即每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。

	x := make([]int, 3, 10)
	fmt.Println(x)
	y := append(x, 10)
	fmt.Println(x) //[0 0 0]
	fmt.Println(y) //[0 0 0 10]
	x = append(x, 6)
	fmt.Println(x) //[0 0 0 6]
	fmt.Println(y) //[0 0 0 6]

map

一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

​内置的make函数可以创建一个map。(这样才能有初始的空间)

ages := make(map[string]int) // mapping from strings to ints

没有用make创建空间的话,就会初始化成零值。map类型的零值是nil,也就是没有引用任何哈希表。

var ages map[string]int
fmt.Println(ages == nil)    // "true"
fmt.Println(len(ages) == 0) // "true"

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常。所以就是在向map存数据前必须先创建map。

使用内置的delete函数可以删除元素:

delete(ages, "alice") // remove element ages["alice"]

所有这些操作是安全的,即使这些元素不在map中也没有关系;如果一个查找失败将返回value类型对应的零值,例如,即使map中不存在“bob”下面的代码也可以正常工作,因为ages[“bob”]失败时将返回0。

ages["bob"] = ages["bob"] + 1 // happy birthday!

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。但是要区分这个零值是不是真的有存的,就得用两个返回结果:

age, ok := ages["bob"]
if age, ok := ages["bob"]; !ok { /* ... */ }

map中的元素并不是一个变量,禁止对map的元素进行取址操作。

_ = &ages["bob"] // compile error: cannot take address of map element

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。


要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。如果需要有序,就要把所有的key拿出来在slice或者数组里排序,然后按遍历数组的顺序来遍历map。


Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。

有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。这个辅助函数可以利用 fmt.Sprintf。

​Map的value类型也可以是一个聚合类型,比如是一个map或slice。

结构体

type …… struct {}

结构体变量的成员可以通过点操作符访问,或者是对成员取地址,然后通过指针访问。

点操作符也可以和指向结构体的指针一起工作:

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"

相当于下面语句

(*employeeOfTheMonth).Position += " (proactive team player)"

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。可以指定成员来初始化。

anim := GIF{LoopCount: nframes}

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:

pp := &Point{1, 2}

它和下面的语句是等价的

pp := new(Point)
*pp = Point{1, 2}

直接用 var x *Point的话相当于这个point的指针指向的空间还没开出来,之后直接对x指向的point赋值的话会出问题。所以要么得new一个指向的空间出来,要么给x赋另一个指针的值。

两个结构体将可以使用==或!=运算符进行比较,一一比较两个结构体的每个成员。可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。

​Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

Circle里有一个匿名成员Point。我们可以说Point类型被嵌入到了Circle结构体。这样 Circle 里就可以直接访问 Point 里的xy,就不用再通过所给的 Point 的名字。要初始化的话,就没法通过Point的名字初始化,只能 Circle{Point{8, 8}, 5}

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值