携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
基本数据类型
Go 语言是一种静态类型的编程语言,在 Go 编程语言中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,可以充分利用内存。编译器在进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么。下面就是Go语言的数据类型:
graph LR
数据类型-->基本类型
基本类型-->布尔型
基本类型-->数值型
基本类型-->字符型
布尔型-->true/false
数值型-->整形
数值型-->浮点型
整形-->int/int8/int16/int32/int64/uint/uint8/uint16/uint32/uint64/byte/uintptr
浮点型-->float32/float64/complex64/complex128
字符型-->string
数据类型-->派生类型
派生类型-->指针
派生类型-->数组
派生类型-->结构体
派生类型-->通道
派生类型-->切片
派生类型-->函数
派生类型-->接口
派生类型-->Map
类型描述如下:
类型 | 描述 |
---|---|
uint | 32位或64位 |
uint8 | 无符号 8 位整型 (0 到 255) |
uint16 | 无符号 16 位整型 (0 到 65535) |
uint32 | 无符号 32 位整型 (0 到 4294967295) |
uint64 | 无符号 64 位整型 (0 到 18446744073709551615) |
int | 32位或64位 |
int8 | 有符号 8 位整型 (-128 到 127) |
int16 | 有符号 16 位整型 (-32768 到 32767) |
int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
byte | uint8的别名(type byte = uint8) |
rune | int32的别名(type rune = int32),表示一个unicode码 |
uintptr | 无符号整型,用于存放一个指针是一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。 uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。 |
float32 | IEEE-754 32位浮点型数 |
float64 | IEEE-754 64位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
示例:
package main
import "fmt"
func main() {
var ui uint = 32
var i int = -32;
var i16 int16 = -1024;
var ui32 uint32 = 1024;
fmt.Println(ui, i, i16, ui32)
//字符只能被单引号包裹,不能用双引号,双引号包裹的是字符串
var c1 byte = 'a'
var c2 byte = '0'
//当我们直接输出type值时,就是输出了对应字符的ASCII码值
//'a' ==> 97
fmt.Println(c1, c2)
//如果我们希望输出对应字符,需要使用格式化输出
fmt.Printf("c1 = %c c2 = %c\n", c1, c2)
var s string = "hello go"
fmt.Println(s)
}
字符串
对于整形,布尔型这些都很普通,这里就不讲了,简单讲一下字符串类型,Go语言的字符串是一个用UTF-8编码的变宽字符序列,它的每一个字符都用一个或多个字节表示 。在Go语言中,没有字符类型(char),但可使用 byte类型表示。
package main
import (
"fmt"
)
func main() {
{
// 字符串操作
// 使用索引号 “[ ]” 返回子串。
// 返回的字符串依然指向原字节数组,仅修改了指针和长度属性
str := "hello, oldboy"
s1 := str[0:5]
s2 := str[7:13]
fmt.Println(s1, s2)
}
{
//修改字符串,可先将其转换成 []rune 或 []byte,
//完成后再转换为 string。无论哪种转换,都会重新分配内存,并复制字节数组。
str1 := "hello baby"
s1 := []byte(str1)
s1[0] = 'H'
fmt.Println(string(s1))
str2 := "鸟宿池边树,僧推月下门。"
s2 := []rune(str2)
s2[7] = '敲'
fmt.Println(string(s2))
}
}
数组
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。
- 通过指定数据类型和元素个数(数组长度)来声明数组。
// 声明一个长度为5的整数数组
var array[5]int
// 声明一个长度为5的整数数组并初始化
var array [5]int = [5]int{1, 2, 3, 4, 5}
- 通过使用数组字面值快速创建和初始化数组,数组字面值允许我们声明我们需要的元素个数并指定数据类型。
// 声明一个长度为5的字符串数组并初始化每个元素
array:= [5]string{"Linux",“Python”,“Java”,“Golang”,“DBA”}
- 把数组长度写成
…
,那么Go编译器将会根据你的元素来推导出数组的长度。
// 通过初始化值的个数来推导出数组容量
array:= [...]int{1,2,3,4,5}
- 如果我们知道想要数组的长度,但是希望对指定位置元素初始化。
// 声明一个长度为5的整数数组,为索引为1和3的位置指定元素初始化,剩余元素为该元素类型的默认值:
array:= [5]int{1:1,3:3}
注意:
- 当一个数组被声明时,它里面包含的每个元素都会被初始化为该元素类型的默认值。
- 一旦数组被声明了,那么它的数据类型跟长度都不能再被改变。
- 如果你需要更多的元素,那么只能创建一个你想要长度的新的数组,然后把原有数组的元素拷贝过去。
package main
import "fmt"
func main() {
arr2 := [5]string{"Linux", "Python", "Java", "Golang", "DBA"}
fmt.Println(arr2)
arr2[0] = "Cpp"
fmt.Println(arr2)
fmt.Println(len(arr2))
}
多维数组
数组总是一维的,但是可以组合成多维的。多维数组通常用于有父子关系的数据或者是坐标系数据:
package main
import "fmt"
func main() {
array := [3][6]int{}
fmt.Printf("数组长度:%d,数组容量:%d\n", len(array), cap(array))
array[0][0] = 0
array[0][1] = 1
array[1][0] = 2
array[1][1] = 3
fmt.Println(array)
//声明一个二维数组
var array1 [3][6]int
fmt.Println(array1)
//使用数组字面值声明并初始化
array2 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
fmt.Println(array2)
//指定外部数组索引位置初始化
array3 := [4][2]int{1: {20, 21}, 3: {40, 41}}
fmt.Println(array3)
//同时指定内外部数组索引位置初始化
array4 := [4][2]int{1: {20, 21}, 3: {40, 41}}
fmt.Println(array4)
}
指针
go的指针和c语言的指针类型,都是表示一个变量的地址,不同的是,go的指针要比c的指针简单的多,老规矩,代码注释,如下:
package main
import "fmt"
func main() {
var count = 100 //定义变量count
var ptr *int //定义一个指针ptr,此指针可以保存int类型变量的地址
ptr = &count //ptr保存的是变量count的地址, & 符号是取变量地址的符号
fmt.Println("count=",count) //打印count的值
fmt.Println("ptr=", *ptr) //打印ptr指向的变量的值,此句打印100
}
指针(pointer)在Go语言中可以被拆分为两个核心概念:
- 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
slice
因为数组的长度定义后不可修改,所以需要切片来处理可变长数组数据。切片可以看作是一个可变长的数组,是一个引用类型。
它包含三个数据:
- 指向原生数组的指针
- 切片中的元素个数
- 切片已分配的存储空间大小
package main
import "fmt"
func main() {
var sl []int // 声明一个切片,不需要指定大小
sl = append(sl, 1, 2, 3) // 往切片中追加值
fmt.Println(sl) // 输出:[1 2 3]
var arr = [5]int{1, 2, 3, 4, 5} // 初始化一个数组
var sl1 = arr[0:2] // 冒号:左边为起始位(包含起始位数据),右边为结束位(不包含结束位数据);不填则默认为头或尾
var sl2 = arr[3:]
var sl3 = arr[:5]
fmt.Println(sl1) // 输出:[1 2]
fmt.Println(sl2) // 输出:[4 5]
fmt.Println(sl3) // 输出:[1 2 3 4 5]
sl1 = append(sl1, 11, 22) // 追加元素
fmt.Println(sl1) // 输出:[1 2 11 22]
}
也可以使用make关键字,语法如下:
make([]类型, 大小,预留空间大小),make() 函数用于声明slice切片、map字典、channel通道。
package main
import "fmt"
func main() {
var s1 = make([]int, 5) // 定义元素个数为5的切片
s2 := make([]int, 5, 10) // 定义元素个数5的切片,并预留10个元素的存储空间(预留空间不知道有什么用?)
s3 := []string{`aa`, `bb`, `cc`} // 直接创建并初始化包含3个元素的数组切片
fmt.Println(s1, len(s1)) // 输出:[0 0 0 0 0] 5
fmt.Println(s2, len(s2)) // 输出:[0 0 0 0 0] 5
fmt.Println(s3, len(s3)) // [aa bb cc] 3
s1[1] = 1 // 声明或初始化大小中的数据,可以指定赋值
s1[4] = 4
//s1[5] = 5 // 编译报错,超出定义大小
s1 = append(s1, 5) // 可以追加元素
fmt.Println(s1, len(s1)) // 输出:[0 1 0 0 4 5] 6
s2[1] = 1
s2 = append(s2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
fmt.Println(s2, len(s2)) // 输出:[0 1 0 0 0 1 2 3 4 5 6 7 8 9 10 11] 16
// 遍历切片
for i := 0; i < len(s2); i++ {
v := s2[i]
fmt.Printf("i: %d, value:%d \n", i, v)
}
}
注: 了解C++和Java的同学,可以参考vector和List,切片就是类似这两个数据结构
Map
map
是一种键值对的无序集合,与 slice
类似也是一个引用类型。map 本身其实是个指针,指向内存中的某个空间。
声明方式与数组类似,声明方式:var 变量名 map[key类型值类型 或直接使用 make 函数初始化:make(map[key类型]值类型, 初始空间大小)。其中key值可以是任何可以用==判断的值类型,对应的值类型没有要求。
package main
import (
"fmt"
"unsafe"
)
func main() {
// 声明后赋值
var m map[int]string
fmt.Println(m) // 输出空的map:map[]
//m[1] = `aa` // 向未初始化的map中赋值报错:panic: assignment to entry in nil map
// 声明并初始化,初始化使用{} 或 make 函数(创建类型并分配空间)
var m1 = map[string]int{}
var m2 = make(map[string]int)
m1[`a`] = 11
m2[`b`] = 22
fmt.Println(m1) // 输出:map[a:11]
fmt.Println(m2) // 输出:map[b:22]
// 初始化多个值
var m3 = map[string]string{"a": "aaa", "b": "bbb"}
m3["c"] = "ccc"
fmt.Println(m3) // 输出:map[a:aaa b:bbb c:ccc]
// 删除 map 中的值
delete(m3, "a") // 删除键 a 对应的值
fmt.Println(m3) // 输出:map[b:bbb c:ccc]
// 查找 map 中的元素
v, ok := m3["b"]
if ok {
fmt.Println(ok)
fmt.Println("m3中b的值为:", v) // 输出:m3中b的值为: bbb
}
// 或者
if v, ok := m3["b"]; ok { // 流程处理后面讲
fmt.Println("m3中b的值为:", v) // 输出:m3中b的值为: bbb
}
fmt.Println(m3["c"]) // 直接取值,输出:ccc
// map 中的值可以是任意类型
m4 := make(map[string][5]int)
m4["a"] = [5]int{1, 2, 3, 4, 5}
m4["b"] = [5]int{11, 22, 33}
fmt.Println(m4) // 输出:map[a:[1 2 3 4 5] b:[11 22 33 0 0]]
fmt.Println(unsafe.Sizeof(m4)) // 输出:8,为8个字节,map其实是个指针,指向某个内存空间
}
类型别名
定义类型别名的写法为:
type TypeAlias = Type
类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?下面通过一段代码来理解。
package main
import (
"fmt"
)
// 将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 type: main.NewInt
a2 type: int