Go语言面试系列:Go基础类型大全
go语言自带的基础类型包括
- int :有符号的整数类型,具体占几个字节要看操作系统的分配,不过至少分配给32位。
- uint:非负整数类型,具体占几个字节要看操作系统的分配,不过至少分配给32位。
- int8:有符号的整数类型,占8位bit,1个字节。范围从负的2的8次方到正的2的8次方减1。
- int16:有符号的整数类型,占16位bit,2个字节。范围从负的2的16次方到正的2的16次方减1。
- int32:有符号的整数类型,占32位bit,4个字节。范围从负的2的32次方到正的2的32次方减1。
- int64:有符号的整数类型,占64位bit,8个字节。范围从负的2的64次方到正的2的64次方减1。
- uint8:无符号的正整数类型,占8位,从0到2的9次方减1.也就是0到255.
- uint16:无符号的正整数类型,占16位,从0到2的8次方减1.
- uint32:无符号的正整数类型,占32位,从0到2的32次方减1.
- uint64:无符号的正整数类型,占64位,从0到2的64次方减1.
- uintptr:无符号的储存指针位置的类型。也就是所谓的地址类型。
- rune :等于int32,这里是经常指文字符。
- byte:等于uint8,这里专门指字节符
- string:字符串,通常是一个切片类型,数组内部使用rune
- float32:浮点型,包括正负小数,IEEE-754 32位的集合
- float64:浮点型,包括正负小数,IEEE-754 64位的集合
- complex64,复数,实部和虚部是float32
- complex128,复数,实部和虚部都是float64
- error,错误类型,真实的类型是一个接口。
- bool,布尔类型
int8 和uint8 后面的8指的是占得位数,因为有符号的第一位作为符号位置,所以它真的可以计数的位置只有7个了,第一位表示正负,无符号的整数显然没有这个烦恼。
go语言的基础组件分为以下几种:
其中按照是否是引用类型(指针类型)分为引用类型和非引用类型,他们分别是
引用类型 | 非引用类型 |
---|
slice,interface,chan,map | array,func,struct,大部分的内置类型 |
这里要说明以下,所有的非引用类型的初始化值都是一个具体的值,只有引用类型的初始化是nil ,nil在go里面就是指的是空。是不能直接使用的。
全局变量,引用类型的分配在堆上,值类型的分配在栈上。
局部变量,一般分配在栈上。如果局部变量太大,则分配在堆上。如果函数执行完,仍然有外部引用此局部变量,则分配在堆上。
我们分别来介绍以下他们。
array
数组,我们先看一下数组的初始化。
a := [6]string{}
var a [6]string
这里稍微提一下,在go里面的赋值符号有两种:
var a
b :=
其中var 这种方式不论是局部还是全局变量都可以使用,但是后者也就是:= 只有局部变量可以使用。也就是只有函数内部才能使用。
并且,var后面的变量后面的类型是可以省略的,省略后,go会在编译过程中自动判断。所以如果不省略就是长这样.
var a int
说到了变量,go里面当然也有定量,使用const来命名,一般都是全局使用。
const PAI = 12
同样的,定量也可以省略后面的类型。
接下来我们看一下数组的赋值
a[0] = "0"
a[1] = "1"
a[2] = "2"
a[3] = "3"
a[4] = "4"
a[5] = "5"
这里要点明一下,值类型,或则说非引用型类型的“声明” 就等于初始化,也就是说,当你给一个变量声明一个值类型的数据时,就自动给定了初始值。
这里谈一下他们的初始值:
array | int | string | bool | float | func | struct |
---|
空数组,但不是nil,已经占用了声明的数组长度 | 0 | 空字符串,通常我们使用""代指空字符串 | false | 0 | 就是一个空的函数 | 一个空的structure |
length <4 的数组,数据是直接存在栈空间上的,如果数据大于4,那么会存放在静态空间,然后才复制到栈空间上,顺便说一下,堆和栈属于动态存储,其中栈是操作系统直接控制,我们的局部变量,不逃逸的情况下都是存在于栈中,逃逸了就去堆里面了,说完了动态,静态存储区域包含了两者,一个是存储的常量,一个是存储的静态变量,常量好理解就是const来声明的常量,静态变量,比如这里大于4的数组。
数组的语法糖[...]int 使用这种方式,必须在声明的时候直接赋值,否则下面进行赋值的时候,系统不知道你的length到底是多少,就会"out of index"
数组的初始化的中括号里要么是... , 要么就是个常量,不能是变量
关于go里面的比较问题
只有类型一致的情况下,进行比较,比如struct int,等,接口也可以比较,slice map 以及函数体,都是无法进行比较的。chan 可以比较,但是即使是类型一样,也是false的结果,nil也是可以比较的,因为nil的底层是 var nil Type type Type int 也就是说nil其实是一个值类型。nil也是有类型的,比如说接口的nil就是接口类型,那么指针类型的nil就是指针类型,slice的nil就是这个slice类型。
一直在变的无法比较,一成不变的就可以比较,slice和map都因为底层指向可以一直变所以无法比较,函数体内部也是一直可以变,所以只有他们三个无法进行比较。
slice
切片,是一个内置的引用类型,其实质是一个structure,也就是说是一个结构体,这个结构体内部含有一个指向某个数组的地址,所以说我们可以简单的来理解,slice是某个数组的指针。
type SliceHeader struct {
data uintptr
len int
cap int
}
切片的初始化:
a := make([]string,10)
var b [10]string
a := &b
a := []string{1,2,3,}
a := [3]int{1,2,3,}
b := a[0:2]
x := b[0:1]
c := a[:]
这里涉及了几个知识点,首先是make()函数,这个函数是go的内置函数,意义就是为了给引用类型初始化,所以能使用make的就是这些引用类型,slice map chan,interface不可以使用,不好意思它make和new都不能使用。
new的含义就是说从一个值类型上取得它的地址,跟& 相似,后者是取地址的符号。并且new的括号里只能是Type类型。例如:type A map[string]string
切片会重新指向新的数组,比如当leng不够需要扩展,然后cap也不够的时候,就会创建一个新的数组底层,然后进行数据的迁移。
切片使用初始化的方式会在编译期间就完成了初始化,但是当使用make关键字创建的时候就会在运行时初始化,在运行时初始化会对速度造成影响。
当切片非常大,或者切片逃逸了,那么就会在堆上创建这个切片。
切片的扩容【考点】:
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
给定一个期望扩容数字:
- 如果期望大于两倍的老容量,那么新的容量就是这个期待容量
- 如果期望小于两倍的老容量,并且老的容量个数小于1024,那么新的容量就是老容量的二倍
- 如果期望小于两倍的老容量,并且老的容量个数大于1024,那么这个容量就按照之前老容量的1.25倍开始增加,直到大于了期望容量,开始跳出循环
- 如果老容量是小于等于0的,那么新的容量直接等于期望容量
当然这只是初步确定容量,下面还要进行内容的对齐。
切片的数据拷贝:copy(newSlice,oldSlice) ,这里直接是值的拷贝。
谨防切片的数据入侵
谨防,两个切片共用一块公共内存的时候,发生数据的侵入,这个时候可以使用 限制容量 的方式来做到规避这个bug。
package main
import "fmt"
func main() {
A := []int{1, 2, 3, 4, 5, 6, 7}
a := A[:3]
b := A[3:]
fmt.Println("第一次的a和b的数值", a, b)
a = append(a, 21, 22)
fmt.Println("第二次的a和b的值", a, b)
fmt.Println("可以看出,因为a的append并没有超过cap所以说,a和b的底层内存是一块,b的数据被bug更改了")
B := []int{1, 2, 3, 4, 5, 6, 7}
c := B[:3:4]
d := B[3:]
fmt.Println("第一次c和d的值", c, d)
c = append(c, 21, 22)
fmt.Println("第二次测试c和d的值", c, d)
fmt.Println(`这个时候发现,因为在给c定义slice的时候制定了限制的cap,也就是说c的cap被我人为的定义为了4"
"所以c在append的时候就重新指向了一块新的内存地址`)
}
c := B[:3:4] 这段代码是关键,这里的4就是一个限制容量参数。限制c的容量是4。
map
map,也就是所谓的hash map 哈希表,散列表,在go里面的哈希表,使用的避免哈希碰撞的算法是链表法。map的key必须是Type,并且是可以比较的类型,例如int,string,interface{},bool,一般接口类型,不过不可比较的例如 slice map都无法充当key值。
我们来看一下map的初始化
a := make(map[string]string)
make()后面其实是有三个值的,第一个就是类型,第二个是length长度,第三个是cap 容量,你先了解一下,slice是需要指定length值的,也就是第二个值,但是第三个并不需要指定,map更厉害,它只需要提供类型,后面两个都不需要提供。
禁止使用 var a map[string]string来初始化,这种只能是声明一个类型,因为声明后a变量的初始值是nil值,也就是说没有分配内存。
map的使用:
a["0"] = "0"
a["1"] = "1"
a["2"] = "2"
delete(map,key)
value,ok := a["1"]
说到length和cap,我们提一下len()和cap()函数。
这俩函数前者是可以测定array,slice,map的长度,后者是容量,说到长度和容量,听起来很相似,到底啥区别呢?
我们来举个例子:
上文我们谈到,slice的底层是一个数组,那么数组的整个的长度就是这个切片的容量,我们取前三个作为length,那么就是3就是这个slice的长度,所以长度<= 容量,再谈一个点,在go里面的所有关于slice是否 out of index 也就是说是否超过下标,指的都是长度而不是容量。
func main(){
a := make([]string,10,20)
a[10] ="1"
}
panic: runtime error: index out of range [10] with length 10
在map中,有两个数据非常关键,一个是hash function,一个是hash冲突
其中hash函数,分为两种,一种是加密型hash函数,例如rc4,sha,等,一种是非加密型函数,这种函数就是为了检索而生,例如murmur hash 函数。
hash冲突,简单的来说就是两个key使用hash函数计算出来的hash值是一样的,无法去定位真实的value值,那么我们有两种解决的方法,一种是向后寻找法,一种是链表法。
值得注意的是,这里的hash碰撞还不一定就是计算出来的hash值是完全一样,有可能是前某几位是一致的,因为我们取的可能是前几位,不可能取完,
向后寻找意思很简单,我们不论是查找的时候,或者是写的时候都是加入计算出的key,已经有人占据了,那么我们的数据就不放在这个坑了,我们往后找,看是不是有空缺的,如果有就放进去这个key-value数据。
这种方法特别要注意的是装载因子,因为hash表底层存储结构是数组,那么如果数组中的数/长度【这就是装载因子的意义】太大,那么向后寻找法就会很难找到数据,直至时间复杂度变成O(N)
链表法就是key计算出来的hash值一样的放在一个地方,只不过这个地方改成一个链表即可。然后我们查找的时候查找这个链表,看真正要找的key是具体哪个,然后我们取得这个key对应的value即可。这种方法要注意链表过长,过长的意思就是hash函数不行,造成分布不够均匀。或者函数生成的值过于狭窄。
map扩容的两个条件是,一是桶的装载因子超过6.5,二是当溢出桶中的元素数量过多的时候,也需要进行扩容了,如果不扩容就会降低map的性能,其中为了扩容时候的效率,在创建新的数据结构后,因为桶的数量改变了,会重新进行hash计算,并且把旧桶中数据进行分流到新的桶中。
在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流。
哈希表的每个桶都只能存储 8个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。
func
函数,以及后文要谈的方法,都是这种形态。
func main(){
}
func fast(a string,b int)(float64,func (int,string)){
return 0,func()(int,string){
return 1, ""
}
}
func heigh(a int)(b string){
return ""
}
var apple = func()(string){}
上面的函数显示了一个函数的基本样子,func关键字在最前面,后面是函数名称,函数后面的括号里是两个临时变量,再后面是返回的值,注意,如果返回的值只有一个,可以省略括号,并且go可以直接返回多值。
函数的使用:
fast("",0)
调用的时候还是比较容易的。
struct
结构体,我们来看一下基本的形态
type A struct {
v1 ,v2 int
v3 bool
}
使用type 这个符号,加上变量名称,再加上一个struct,然后结构体内部的变量的声明就如同文中所示,一个变量,后面是类型,如果两个变量一致可以放一起。比如v1,v2,中间用, d
我们看一下结构体的使用
var a A
a.v1 = 1
a.v2 = 2
a.v3= true
var a = A {
v1:1,
v2:1,
v3:true,
}
var a = A{
1,3,true,
}
var a = &A{
}
var a = new(A)
interface
go语言的接口,核心就是鸭子🦆 理论,也就是说不像传统语言,java那种必须显示声明出来接口的调用,go语言中只需要实现接口的方法就是实现了这个接口。
声明一个接口
type people interface {
see()
eat()
}
内部的是函数,接口内部只接函数。
在此提示一下 接口与众不同,不可使用make和new来声明获取一个接口,接口并没有实际的任何意义,所以它没有任何的底层指向,使用make是没有意义的。同时因为interface的实质也是一个structure 只是内部是记录的一些参数,所以说取这个接口的地址也没有任何的意义,所以go里面不要取接口的地址。
如何实现这个接口?
func c (p people){
p.see()
p.eat()
}
这里,这个函数的变量是接口类型,那么我们要传入的也应该是people这个接口类型,那么什么是符合这个接口类型呢?
等下文的面向对象再详细说一下。
chan
chan,这里先简单介绍一下,后文go并发编程可以详细介绍一下。
因为chan也是引用类型,所以它也必须使用make才可以初始化
c := make(chan string,2)
chan 后面要加上具体的类型,然后再加上长度即可。
这里你先简单的了解一下chan,中文叫做通道,你可以简单的和unix中的通道类比一下,后面的长度就是指的,通道内可以缓存的数据量,当然这里你把通道当作一个队列。
使用的时候可以这样做。
func fast(){
c := make(chan string,2)
go b(c)
}
func b(chan string){}
这里的go 关键字指的是开辟了一个新的goroutine,然后通道在不同的goroutine中流传传递信息。
其中,有两种特殊的命名方式,就是只读通道和只写通道。
var a <- chan string
var b chan <- string
往通道里读写数据这什么来操作的:
a <- ""
<- a
当然我们一般不会舍弃读取的通道数据,会将数据赋值给一个变量
c := <- a
iota
特殊常量,可以自增。切记,只能用在常量中。
const (
a = 1+ iota
b
c
d
e
f = "12"
g= iota
h
i
)
1 2 3 4 5 12 6 7 8 iota在常量中处于自增的方式,可以看到,iota的初始值是0,所以a等于1,b就是2,b等于1+1,当遇到f的时候,iota自己的自增没有变化,但是f就变了,变成了“12”,然后g又给了iota,那么这个时候的iota就不是从零开始,iota的就是6,意思就是往下数嘛,从0开始,到g就是6了。又因为这个变量的赋值算式不是1+iota,是iota了,那么后面的就变成了直接将iota赋值给变量了。
字符串
go语言中的字符串是一个只读字节切片 ,底层数组里存放的是byte类型,如果我们想改变这个字符串,可以将 字符串 <=> []byte 互相的转换,进而改变这个字符串,这个转化的过程,先将静态存储区的字符串转到栈或者堆中(数组较大就会转化到堆上)然后string转变为字节数组,更改数组中元素,再转为字符串即可。因为字符串作为只读的类型,我们并不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。
在两者进行数据转化的时候,是有性能的损失的,所以优化性能的话,可以选择减少字符串和[]byte之间的转换。
func main() {
a := "github.com/shgopher"
b := []byte(a)
b[0]= 12
fmt.Println(string(b))
}
|