0%

Go语言学习笔记--类型篇

前言:
  学习任何一门语言都需要掌握它都数据类型,就像java语言中包含8种基本数据类型、有变量常量之分,go语言也如此。文档主要记录《GO语言学习笔记》一书都学习笔记、demo,以便后期回顾学习。

2.类型篇
2.1变量

go语言用关键字var定义变量,跟java不同,go语言类型放在变量名后。如果显示提供初始化值,则可以省略变量类型,由编译器推断。同时,可一次定义多个变量,包括用不同初始值定义不同类型。

1
2
3
4
5
6
var x int 	//定义变量,指定类型为int,如果没有初始化值,运行时会自动初始化为二进制零值

var y = false //显示提供初始化值false,可以省略变量类型(bool),由编译器推断

var a,b int //一次定义相同类型都多个变量
var a,s=100,"abc" //用不同初始值定义不同类型

除var关键字外,还可以使用更加简短的变量定义和初始化语法。

1
2
3
4
func main(){
x := 100
a,s := 1,"abc"
}

简短模式定义变量时,需要注意以下限制:

  • 定义变量,同时显示初始化
  • 不能提供数据类型
  • 只能用在函数内部
2.2命名

  同其他编程语言一样,对变量、常量、函数、自定义类型进行命名,通常优先选用有实际含义,易于理解对字母或单词组合。
命名建议:

  • 以字母或下划线开始,由多个字母、数字和下划线组合而成
  • 区分大小写
  • 使用驼峰命名格式
  • 局部变量优先使用短名
  • 不可以使用go语言保留的关键字
  • 不建议使用与预定义常量、类型、内置函数相同的名字
  • 专有名词通常会全部大写,例如HTML

  go语言没有java语言中的访问修饰符(public、protected、private),因此采用首字母大小写来决定其作用域。首字母大写的为导出成员,可被包外引用,首字母小写仅能在包内访问。
空标识符
  和Python类似,Go也有哥名为“_”的特殊成员。通常作为忽略占位符使用,可作为表达式左值,用“_”接收的值,表示舍弃不用,故无法读取其内容。

1
2
3
4
5
import "strconv"
func main(){ //go语言main函数不能有任何参数
x,_ := strconv.Atoi("12") //忽略Atoi的err返回值
println(x)
}
2.3常量

  同java语言一样,go语言也有常量,常量用来表示运行时恒定不变的值。常量值必须是编译期可确定的字符、字符串、数字或布尔值。可指定常量类型,或由编译器通过初始化值推断。
go语言常量值由const修饰:

1
2
const x,y int=123,0x22	//定义两个常量值x,y 其中y为16进制表示
const s = "hello,world!" //定义常量s并且省略其类型,由编译器通过初始化值推断

  可在函数代码块中定义常量,不曾使用的常量不会引发编译错误(变量定义未使用,编译器会报错)。

1
2
3
4
5
6
7
8
9
func main(){
const x = 123
println(x)
const y = 1.23 //未使用,不会引发编译错误
{
const x = "abc" //在不同作用域可以定义同名常量,编译器不会报错
println(x)
}
}

  如果显示指定类型,必须确保常量左右值类型一致,需要时可以做显示类型转换。右值不能超出左边类型取值范围,否则报溢出错误。常量值也可以是某些编译器能计算出结果的表达式,如unsafe.Sizeof、len、cap等。

1
2
3
4
5
6
import "unsafe"
//常量组
const(
ptrSize = unsafe.Sizeof(uintptr(0))
strSize = len("hello,world!")
)

  在常量组中,如果不指定常量的类型和初始化值,则与上一行非空常量右值相同。

1
2
3
4
5
6
7
8
9
10
11
import "fmt"
func main(){
const(
x uint16 = 120
y //y未指定类型和初始化值,与上一行非空常量右值相同,即y=120
s = "abc"
z //与s类型、右值相同
)
fmt.Printf("%T,%v\n",y,y) //输出类型和值
fmt.Printf("%T,%v\n",z,z)
}

输出结果:

1
uint16,120 string,abc

关于占位符的介绍,请参考:Go语言fmt格式“占位符”
枚举:
  Go并没有明确意义上的enum定义,不过可借助iota标识符实现一组自增常量值来实现枚举类型。

1
2
3
4
5
6
7
8
9
10
11
const(
x = iota //0
y //1
z //2
)
const(
_ = iota //0
KB = 1<<(10*iota) //1<<(10*1)
MB //1<<(10*2)
GB //1<<(10*3)
)

  自增作用范围为常量组。可在多常量定义中使用多个iota,它们各自单独计数,只须确保组中每行常量的列数量相同即可。

1
2
3
4
5
const(
_,_ = iota,iota*10 //0,0*10
a,b //1,1*10
c,d //2,2*10
)

如中断iota自增,则必须显示恢复。且后续自增值按行序递增,而非C语言 enum那样按上一取值递增。

1
2
3
4
5
6
7
8
const(
a = iota //0
b //1
c = 100 //100
d //100 与上一行常量右值表达式相同
e = iota //4(恢复iota自增,计数包括c、d)
f //5
)

自增默认数据类型为int,可显示指定类型

1
2
3
4
5
const(
a = iota //int
b float32 = iota //float32
c = iota //int(如不显示指定iota,则与b数据类型相同)
)

常量除“只读”外,和变量究竟有什么不同?

1
2
3
4
5
6
var x = 0x100
const y = 0x200
func main(){
println(&x,x)
println(&y,y) //编译器报错:cannot take the address of y
}

  不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。数字常量不会分配存储空间,无法像变量那样通过内存寻址来取值,因此无法获取地址。

2.4基本类型
类型 长度(字节数) 默认值 说明
bool 1 false 布尔型的值只可以是常量truefalse,默认值false
byte 1 0 类似uint8
int 4或8 0 默认整数类型,根据目标平台,32位或64位
uint 4或8 0 默认无符号整数类型,依据目标平台,32位或64位
int8,uint8 1 0 int8取值范围-128~127,uint8取值范围0-255
int16,uint16 2 0 int16取值范围-3276832767,uint16取值范围065535
int32,uint32 4 0 int32取值范围-21474836482147483647,uint32取值范围04294967295
int64,uint64 8 0 int64取值范围-9223372036854775808 ~ 9223372036854775807,uint64取值范围0 ~ 18446744073709551615
float32 4 0.0 32位浮点数
float64 8 0.0 64位浮点数(默认浮点数类型)
complex64 8 32位实数和虚数
complex128 16 64位实数和虚数
rune 4 0 Unicode Code Point,类似int32
uintptr 4,8 0 无符号整型,用于存储指针
string “” 字符串,默认值为空字符串而非NULL
array 数组
struct 结构体
func nil 函数类型
interface nil 接口
map nil 字典,引用类型
slice nil 切片,引用类型
channel nil 通道,引用类型

支持八进制、十六进制以及科学记数法。标准库math定义了各数字类型的取值范围。

1
2
3
4
5
6
7
import("fmt" "math")
func main(){
a,b,c := 100,0144,0x64 //a 十进制 100,b 八进制 100,c 十六进制 100
fmt.Println(a,b,c)
fmt.Printf("0b%b, %#o, %#x\n",a,a,a) // %b 二进制占位符 %o 八进制表示,%#o 为八进制添加前导0, %x 十六进制表示
fmt.Println(math.MinInt8,math.MaxInt8)
}

输出结果:

1
2
3
4
100,100,100
0b1100100, 0144, 0x64
-128 127

标准库strconv可以在不同进制(字符串)间转换

1
2
3
4
5
6
7
8
9
10
a, err := strconv.ParseInt("1100100",2,32)
if err != nil{
println(err.Error())
}
b, _ := strconv.ParseInt("0144",8,32)
c, _ := strconv.ParseInt("64",16,32)
println(a,b,c)
println("0b" + strconv.FormatInt(a,2))
println("0"+strconv.FormatInt(a,8))
println("0x" + strconv.FormatInt(a,16))

输出结果:

1
2
3
4
5
100,100,100
0b1100100
0144
0x64

别名:在官方的语言规范中,专门提到两个别名。byte 是 uint8别名;rune是int32别名。别名类型无须进行类型转换,可直接赋值。

1
2
3
4
5
6
7
8
9
func test(x byte){
println(x)
}
func main(){
var a byte = 0x11
var b uint8 = a
var c uint8 = a+b
test(c)
}

但这并不表示,拥有相同底层结构但就属于别名。就算在64位平台上int和int64结构完全一致,也分属不同类型,须显示转换。

1
2
3
4
5
6
7
8
func add(x,y int) int {
return x+y
}
func main() {
var x int = 100
var y int64 = x //编译器报错:cannot use x(type int) as type int64 in assignment
add(x,y)//编译器报错:cannot use y(type int64)as type int in argument to add
}
2.5引用类型

所谓引用类型(reference type) 特指 slice、map、channel这三种预定义类型。
  相比数字、数组等类型,引用类型拥有更复杂但存储结构。除分配内存外,它们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。
  内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//slice
func mkslice() []int {
s := make([]int,0,10)
s = append(s, 100)
return s
}
//map
func mkmap() map[string]int {
m := make(map[string]int)
m["a"] = 1
return m
}
func main(){
m := mkmap()
println(m["a"])
s := mkslice()
println(s[0])
}
2.6类型转换
  1. 隐式转换造成的问题远大于它带来的好处。。。
      除常量、别名类型以及未命名类型外,Go强制要求使用显示类型转换。加上不支持操作符重载,所以我们总是能确定语句及表达式的明确含义。
    1
    2
    3
    a := 10
    b := byte(a)
    c := a + int(b) //混合类型表达式,必须确保类型一致,也可写成 c := byte(a) + b
  2. 同样不能将非bool类型结果当作true/false使用
    1
    2
    3
    4
    5
    6
    func main(){
    x := 100
    var b bool = x //错误:cannot use x(type int)as type bool in assignment
    if x { //错误:non-bool x(type int)used as if condition
    }
    }
  3. 语法歧义
    如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以免造成语法分解错误。
    1
    2
    3
    4
    5
    6
    func main(){
    x := 100
    //p := *int(&x) //错误:Invalid indirect of 'int(&x)' (type 'int')
    //Cannot convert expression of type *int to type int
    p := (*int)(&x) //转换的目标是指针,必须使用括号
    }
2.7自定义类型

使用关键字type定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等。

1
2
3
4
5
6
7
8
9
10
11
import ("fmt")
type flags byte
const(
read flags = 1<<iota
write
exec
)
func main(){
f := read|exec //位运算 “或”
fmt.Printf("%was b\n",f) //输出二进制标记位
}

输出:

1
101

和var、const类似,多个type定义可合并成组,可在函数或代码块内定义局部类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main(){
type(//定义组
user struct{//结构体
name string
age uint8
}
event func(string) bool //函数类型
)
u := user{"Tom",20}
fmt.Println(u)
var f event = func(s string) bool{
println(s)
return s != ""
}
f("abc")
}

未命名类型
  与有明确标识符等bool、int、string等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等属性相关,故称作未命名类型(unnamed type)。当然,可用type为其提供具体名称,将其改变为命名类型(named type)。
具有相同声明等未命名类型被视为同一类型。

  • 具有相同基类型的指针
  • 具有相同元素类型和长度的数组(array)
  • 具有相同元素类型的切片(slice)
  • 具有相同健值类型的字典(map)
  • 具有相同数据类型及操作方向的通道(channel)
  • 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体(struct)
  • 具有相同签名(参数和返回值列表,不包括参数名)的函数(func)
  • 具有相同方法集(方法名、方法签名,不包括顺序)的接口(interface)

容易被忽视的是struct tag,它也属于类型组成部分,而不仅仅是元数据描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main(){
var a struct{//匿名结构类型
x int `x`
s string `s`
}
var b struct{
x int
s string
}
b = a //错误:cannot use a type struct{x int "x";s string "s"} as type struct{x int;s string}in assignment

fmt.Println(b)
}