Go语言中,数据类型用于声明函数与变量、常量的数据的类型。
Go语言的基本数据类型包括整型、浮点型、字符与字符串、布尔型等,另外,还要了解数据类型之间的转换方法、指针与类型别名的使用等。
1、整型
var value1 int32 //全局声明
func main() {
value2 := 64 //函数内部声明
}
有符号整数类型 | 无符号整数类型 |
int8(8bit) | unit8 |
int16(16bit) | uint16 |
int32 (32bit) | uint32 |
int64 (64bit) | uint64 |
Go语言中,除了指定的整型之外,还有int(符号整数)和uint(无符号整数)两种整数类型。在实际开发中,由于编译器和计算机硬件的不同,所以,它们的字节长度也是有所差异的,int和uint所能表示的整数大小在32bit或64bit之间变化。
另外,还有一种无符号的整数类型uintptr,它没有指定具体的bit大小,但是足以容纳指针。uintptr类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。
使用int和uint的情况如下:
- 程序的逻辑对整型的范围没有特殊需求。例如,对象的长度使用内建len()函数返回,这个长度可以根据不同平台的字节长度进行变化。实际使用中,切片或map的元素数量等都可以用int来表示。
- 在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不使用int和uint。
整型的位运算符:
2、浮点型
Go语言提供了两种精度的浮点数,即float32和float64:
- float32的浮点数的最大范围约为3.4e38,常量定义为math.MaxFloat32。
- float64的浮点数的最大范围约为1.8e308,常量定义为math.MaxFloat64。
在使用浮点型时需要注意以下几点:
- 浮点数的字面量被自动类型推断为float64类型,例如:
var a := 20.00
- 计算机很难进行浮点数的精确表示和存储,因此两个浮点数之间不应该使用==或!=进行比较操作,如果需要高精度的科学计算,应该使用math标准库。
- 浮点数在进行声明时,可以只写整数部分或者小数部分,例如:
const e = .71828 //0.71828
const f = 1. //1
用Printf函数打印浮点数时可以使用“%f”来控制保留几位小数,例如:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) // 按照默认宽度和精度输出整型
fmt.Printf("%.2f\n", math.Pi) //按照默认 宽度 、2位精度输出(小数点后的位数)
}
3、字符与字符串
字符串是不可改变的字节序列,字符串可以包含任意数据,但是通常包含可读的文本。字符串是UTF-8字符的一个序列(当字符为ASCII码表上的字符时则占用1B,其他字符根据需要占用2~4B)。
UTF-8是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括XML和JSON在内也都使用该编码。由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用1~4B,这与其他编程语言如C++、Java或Python不同(Java始终使用2B)。Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其他语言那样需要对使用UTF-8字符集的文本进行编码和解码。
字符串中的每一个元素称为“字符”,在遍历或者单个获取字符串元素时可以获得字符。
Go语言的字符有以下两种:
- uint8类型或byte型,代表ASCII码的一个字符。
- rune类型,代表一个UTF-8字符,当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型等价于int32类型。
a. 字符串的转义字符
b. 定义多行字符串
在Go语言中,字符串的常见表达方式之一就是使用双引号书写字符串的方式,这种方式被称为字符串字面量(string literal),使用该方式时需要注意双引号字面量不能跨行。如果想要在源码中嵌入一个多行字符串,就必须使用“`”反引号,代码如下:
func main() {
const str = `第一行
第二行
第三行
\r\n
`
fmt.Println(str)
}
- 在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
- 多行字符串一般用于内嵌源码和内嵌数据等
- 在“`”间的所有代码均不会被编译器识别,而只是作为字符串的一部分。
c. 连接字符串
在Go语言中可以使用“+”连接字符串
func main() {
s := "adcd 你"
fmt.Println(s[4:] + "好")
str := "哎一古," +
"金社长"
fmt.Println(str)
}
另外,字符串还可以使用“==”和“<”等符号进行比较,通过比较逐字节的编码获取结果
func compare() {
a := "你"
b := "好"
if a < b {
fmt.Println(a[0], b[0])
fmt.Println(a[1], b[1])
fmt.Println(a[2], b[2])
}
c := "a"
d := "b"
if c < d {
fmt.Println(c[0], "小于", d[0])
}
}
- 在以上代码中,字符串的大小判断是根据第一字节来判定的,“你好”之所以比“你”“好”小,是因为“你”的第一字节是228,而“好”的第一字节是229。
d. 字符串的修改
在Go语言中,字符串的内容不能修改,这就意味着不能使用a[i]这种方式修改字符串中的UTF-8编码,如果确实要修改,那么可以将字符串的内容复制到另一个可写的变量中,然后进行修改。一般使用[]byte或[]rune类型。
如果要对字符串中的字节进行修改,则转换为[]byte格式,如果要对字符串中的字符进行修改,则转换为[]rune格式,转换过程会自动复制数据。
修改字符串中的字节(使用[]byte):
func mod_byte() {
a := "Hello 世界!"
b := []byte(a) // 转换为[]byte,自动复制数据
b[5] = ',' // 修改[]byte
fmt.Printf("%s\n", a) // a 不能被修改,内容保持不变
fmt.Printf("%s\n", b) // 修改后的数据
}
修改字符串中的字符(使用[]rune):
func mod_character() {
a := "Hello 世界!"
b := []rune(a) // 转换为[]rune,自动复制数据
b[6] = '中' // 修改[]rune
b[7] = '国' // 修改[]rune
fmt.Println(a) // a不能被修改,内容保持不变
fmt.Println(string(b)) //转换为字符串,右一次复制数据
}
e. 字符串格式化
在Go语言中,单个字符可以使用单引号(')来创建,字符串支持切片操作。但是需要注意的是,如果字符串都是由ASCII字符组成的,可以随便使用切片进行操作;如果字符串中包含其他非ASCII字符,直接使用切片获取想要的单个字符时应十分小心,因为对字符串直接使用切片时是通过字节进行索引的,而非ASCII字符在内存中可能不是由1B组成的。如果想对字符串中的字符依次访问,可以使用range操作符。2
4、布尔型
布尔型的常量和变量也可以通过逻辑运算符(非!、和&&、或||)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的Go语言语句。
逻辑值可以被用于条件结构中的条件语句,以便测试某个条件是否满足。另外,和(&&)、或(||)、相等(==)、不等(!=)属于二元运算符,而非(!)属于一元运算符。这里使用T代表条件符合的语句,用F代表条件不符合的语句。
a. 非运算符(!)
非运算符用于取得和布尔值相反的结果。
!T -> false
!F -> true
b. 和运算符(&&)
只有当两边的值都为true时,和运算符的结果才是true。
T && T-> true
T && F-> false
F && T-> false
F && F->false
c. 或运算符(&&)
只有当两边的值都为false时,或运算符的结果才是false,其中任意一边的值为true就能够使得该表达式的结果为true。
T || T -> true
T || F -> true
F || T -> true
F || F -> false
在Go语言中,&&和||是具有快捷性质的运算符,当运算符左边表达式的值已经能够决定整个表达式的值时(&&左边的值为false,||左边的值为true),运算符在右边的表达式将不会被执行。利用这个性质,如果有多个条件判断,应当将计算过程较为复杂的表达式放在运算符的右侧,以减少不必要的运算。
另外,利用括号可以提升某个表达式的运算优先级。在格式化输出时,可以使用%t来表示要输出的值为布尔型。
5、数据类型转换
在必要及可行的情况下,一个类型的值可以被转换成另一种类型的值。类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
Go语言类型转换的基本格式如下:type_name(expression)
a := 5.0
b := int(a)
类型转换只能在定义正确的情况下转换成功:
- 从一个取值范围较小的类型转换到一个取值范围较大的类型(将int16转换为int32)。
- 当从一个取值范围较大的类型转换到取值范围较小的类型时(将int32转换为int16或将float32转换为int),会发生精度丢失(截断)的情况。
- 只有相同底层类型的变量之间可以进行相互转换(如将int16类型转换成int32类型),不同底层类型的变量相互转换时会引发编译错误(如将bool类型转换为int类型)。
func main() {
// 输出个数值范围
fmt.Println("int8 range:", math.MinInt8, math.MaxInt8)
fmt.Println("int16 range:", math.MinInt16, math.MaxInt16)
fmt.Println("int32 range:", math.MinInt32, math.MaxInt32)
fmt.Println("int64 range:", math.MinInt64, math.MaxInt64)
// 初始化一个32位整型值
var a int32 = 1047483647
// 输出变量的十六进制形式和十进制值
fmt.Printf("int32: 0x%x %d\n", a, a)
// 将a变量数值转换为16进制,发生数值阶段
b := int16(a)
// 输出变量的十六进制形式和十进制值
fmt.Printf("int16: 0x%x %d\n", b, b)
// 将常量保存为float32类型
var c float32 = math.Pi
// 转换为int类型,浮点发生精度丢失
fmt.Println(int(c))
}
根据运行结果,16位有符号整型的范围为-32 768~32 767,而变量a的值1 047 483 647不在这个范围内。1 047 483 647对应的十六进制为0x3e6f54ff,转为int16类型后,长度缩短一半,也就是在十六进制上砍掉一半,变成0x54ff,对应的十进制值为21759。
6、类型别名
类型别名是Go 1.9版本中新增的功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。
在Go 1.9版本之前定义内建类型的代码书写方式如下:
type byte uint8
type rune int32
在Go 1.9版本之后定义内建类型的代码书写方式修改为如下:
type byte = uint8
type rune = int32
a. 类型别名与类型定义
定义类型别名的书写格式如下:type TypeAlias = Type
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。
type name = string //类型别名
type name string //类型声明
type name string
将name定义为一个新的类型,该类型拥有和string一样的特性,但是两者是不同的类型,不可用“+”进行拼接等运算。type name = string
将name定义为string的一个别名,使用name和string相同。二者可以当作同一种类型运算。别名只在源码中存在,编译完成后,不会有别名类型。
类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些:
// 将NewInt定义为int类型
type NewInt int
// 给int取一个别名 IntAlias
type IntAlias = int
func main() {
// 将 a 声明为 NewInt 类型
var a NewInt
// 查看 a 的类型名
fmt.Printf("a type: %T\n", a)
// 将 a2 声明为 IntAlias 类型
var a2 IntAlias
// 查看 a2 的类型名
fmt.Printf("a2 type: %T\n", a2)
}
- a的类型是main.NewInt,表main包下定义的NewInt类型,
- a2的类型是int,IntAlias类型只会在代码中存在,编译完成时,不会有IntAlias类型。
b. 非本地类型不能定义方法
能够随意为各种类型起名字,是否意味着可以在自己的包中为这些类型任意添加方法呢?
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
从以上结果中可以看出,编译出错。编译器提示:不能在一个非本地的类型time.Duration中定义新的方法,非本地类型指的就是time.Duration不是在main包中定义的,而是在time包中定义的,与main包不在同一个包中,因此不能为不在一个包中的类型定义方法。
解决这个问题有以下两种方法:
- ① 将代码修改为
type MyDuration time.Duration
,也就是将MyDuration从别名改为类型。 - ② 将MyDuration的别名定义放在time包中。
c. 在结构体成员嵌入时使用别名
当类型别名作为结构体嵌入的成员时会发生什么情况呢?
// 定义商标结构
type Brand struct {
}
// 为商标结构添加Show()方法
func (b Brand) Show() {
}
// 为Brand定义一个别名FakeBrand
type FakeBrand = Brand
// 定义车辆结构
type Vehicle struct {
// 嵌入两个结构
FakeBrand
Brand
}
func main() {
// 声明变量a为车辆类型
var a Vehicle
// 指定调用FakeBrand的Show
a.FakeBrand.Show()
// 取a的类型反射对象
ta := reflect.TypeOf(a)
// 遍历a的所有成员
for i := 0; i < ta.NumField(); i++ {
// a的成员信息
f := ta.Field(i)
// 打印成员的字段名和类型
fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.Name())
}
}
- 第25行,显式调用Vehicle中FakeBrand的Show()方法。
- FakeBrand是Brand的一个别名,在Vehicle中嵌入FakeBrand和Brand并不意味着嵌入两个Brand,FakeBrand的类型会以名字的方式保留在Vehicle的成员中。
如果尝试将第25行改为:a.Show()
编译器将发生错误
- 在调用Show()方法时,因为两个类型都有Show()方法,所以会发生歧义,证明FakeBrand的本质确实是Brand类型。