程序员社区

Go语言面试系列:Go基础类型大全

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

数组,我们先看一下数组的初始化。

// 给数组进行初始化

// 初始化的方式1
a := [6]string{}

// 初始化的方式2
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取了数组的全部数据
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] // 这里的4就是一个限制容量参数。限制c的容量是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"
// 删除key
delete(map,key)

// ok表示是否含有这个值。
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

函数,以及后文要谈的方法,都是这种形态。

//1
func main(){

}
//2
func fast(a string,b int)(float64,func (int,string)){

    return 0,func()(int,string){
        return 1, ""
    }
}
//3
func heigh(a int)(b string){
    return ""
}
//4
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, // 这里注意一下,逗号一定要有,结尾处有逗号
}

// 也可以省略前面的key,但是这样必须按照顺序
var a = A{
    1,3,true,
}

// 如果想取结构体的地址也是有两种方式的
var a = &A{
    //xxx
}

// 另一种
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 8iota在常量中处于自增的方式,可以看到,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))
}
赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go语言面试系列:Go基础类型大全

一个分享Java & Python知识的社区