前言
这篇文章原本计划在8月份发布,但由于面试入职的节奏太快,再加上近期的确繁忙,所以略有延迟。不过,好饭不怕晚,我依然践行了我的承诺。我的原则是,承诺可能因外界原因延后,但绝不会缺席。感谢大家的耐心等待与支持!
Go语言有哪些数据类型?
-
基本数据类型
- 整数类型:Go语言支持多种整数类型,包括int、uint、int8、int16、int32、int64、uint8、uint16、uint32和uint64。其中,int类型的大小取决于平台(32位或64位),而其他类型则表示固定宽度的有符号和无符号整数。
- 浮点数类型:包含float32和float64两种类型,分别对应32位和64位浮点数。此外,complex64和complex128类型用于表示复数,其实部和虚部分别为float32和float64。
- 布尔类型:布尔类型bool代表真或假,其值为true或false。在Go语言中,布尔类型被广泛用于条件判断和逻辑运算。
- 字符串类型:字符串类型string用于表示文本数据,是不可变的序列。在Go中,字符串可以用双引号或反引号定义,并支持原始字符串的操作。
-
特殊数据类型
- 字符类型:字符类型rune用于表示单个Unicode字符。在处理文本数据时,rune类型确保每个字符可以表示为一个Unicode码点,避免了传统字节类型可能出现的编码问题。
- 错误类型:错误类型error是一个接口类型,用于表示函数或方法中的错误。通过使用error类型,Go语言鼓励开发者显式地处理错误,而不是忽略它们。
- 空类型:空类型struct{}用来表示一个空的结构体,不包含任何字段。它通常用于同步操作中,如通道或互斥锁的初始化。
- 切片类型:切片类型是Go中的一种动态大小的序列,可以用来处理数组。切片提供了灵活的数组操作,如自动扩容和截取。
- 映射类型:映射类型map表示无序的键值对集合,其中的键和值可以是任意类型。映射类型提供了高效的数据存储和检索方式,适用于需要快速访问和更新数据的场景。
Byte和Rune有什么区别?
byte和rune在go语言中都是字符类型,且都是别名类型。
byte型本质上是uint8类型的别名,代表了ASCII码的一个字符。
rune型本质上是int32类型的别名,代表一个UTF-8字符。
Go语言中new和make的区别?
在Go语言中new和make都是用于变量创建和初始化的内置函数,但它们在使用场景和行为上存在显著的差异。
-
适用类型
- new:适用于基本类型、数组、结构体等类型的内存分配。
- make:专用于切片、映射和通道这三种引用类型的初始化。
-
返回值
- new:返回指向新分配零值的指针。
- make:直接返回初始化后的引用类型本身。
-
初始化状态
- new:创建的变量是指定类型的零值。
- make:根据类型进行初始化,例如为切片预设长度和容量,或为映射预留空间。
-
底层实现
- new:底层使用runtime.newobject函数来分配内存并清零。
- make:对于切片、映射和通道,分别使用
runtime.makeslice
、runtime.makemap
和runtime.makechan
进行更复杂的初始化工作。
-
性能特点
- new() :分配内存较快,但不进行初始化。
- make() :需要初始化,可能相对较慢,但返回的对象可以直接使用。
-
最佳实践
- new() :当需要指针类型时使用,尤其是用户定义的类型。
- make() :对于Go语言的内置引用类型应优先使用make()进行初始化。
Go中有哪些方式安全读写共享变量
在Go语言中,安全地读写共享变量是并发编程的核心问题之一。Go提供了多种机制来保证在多goroutine环境下对共享资源的安全访问,这些方法包括使用互斥锁、读写锁、原子操作、通道以及sync.Once等。以下是具体介绍:
- 互斥锁:互斥锁(Mutex)是最基础的同步工具,用于保护一个或多个共享变量,防止多个goroutine同时访问导致的竞态条件。当一个goroutine获取了互斥锁后,其他试图获取该锁的goroutine将会被阻塞,直到第一个goroutine释放锁。
- 读写锁:读写锁(RWMutex)允许同时有多个读锁定和一个写锁定。当goroutine进行写操作时,其他读写操作都会被阻塞;但在读锁定的情况下,允许多个goroutine同时读取数据,这在读密集型应用中可以显著提高性能。
- 原子操作:对于一些简单的数据类型,如整数和指针,可以使用
sync/atomic
包提供的原子操作函数来保证操作的原子性,从而避免使用锁带来的性能开销。原子操作可以在不使用互斥锁的情况下保证操作的并发安全性。 - 通道通信:Go提倡“通过通信共享内存”,而非“通过共享内存来通信”。使用通道可以安全地在不同goroutine之间传递数据,通道的操作本身就保证了数据的并发安全。这是一种轻量级的同步机制,尤其适用于生产者消费者模式的数据交换场景。
- 初始化:如果共享变量只需要被初始化一次,可以使用
sync.Once
来确保只执行一次初始化操作。这在全局变量初始化、配置加载等场景下非常有用,可以避免重复初始化带来的问题。
CSP模型是什么?
CSP模型是”以通信的方式来共享内存“,不同与传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯channel(管道)进行通信的并发模型。
GPM分别是什么、分别有多少数量?
- G(Goroutine):即Go协程,每个go关键字都会创建一个协程
- M(Machine):工作线程,在Go中称为Machine,数量对应真实的CPU数(真正干活的对象)
- P(Processor):处理器(Go中定义的一个概念,非CPU),包含运行Go代码的必要资源,用来调度G和M之间的关联关系,其数量可以通过GOMAXPROCS()来设置,默认为核心数。
M必须拥有P才可以执行G中的代码,P含有一个包含多G的队列,P可以调G交由M执行。
Go中对nil的Slice和空的Slice的处理是一致的吗?
- slice := make([]int, 0):slice不为nil,但是slice没有值,slice的底层空间是空的。
- slice := []int{}:slice的值是nil,可用于需要返回的slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
Golang的内存模型中为什么小对象多了会造成GC压力?
通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。
Channel为什么可以做到线程安全
Channel可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。
GC的触发条件
-
主动触发(手动触发),通过调用runtime.GC来触发GC,此调用阻塞式地等待当前GC运行完毕。
-
被动触发,分为两种方式:
- 使用系统监控,当超过两分钟没有产生任何GC,强制触发GC。
- 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已到达阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。
常用语法糖
-
简短变量声明
:=
-
在使用
:=
进行多变量赋值时,如果赋值的变量列表中同时包含已声明和未声明的变量,那么已声明的变量将会被重新赋值。然而,如果赋值列表中只包含已经声明的变量,编译器会报错提示“no new variables on left side of :=”。(为了避免混淆和提高代码的可读性,建议在编码实践中避免示例一这种混用的做法)示例一
a, b := 1, 2 b, c := 3, 4 fmt.Println(a, b, c)
1 3 4
示例二
a, b := 1, 2 a, b := 3, 4
no new variables on left side of :=
-
不能使用函数外部
:=
这种简短变量声明只能用于函数中,用来初始化全局变量是不行的。编译器会报“syntax error: non-declaration statement outside function body”。
-
-
可变参数函数
func Greeting(perfix string, who ...string) { if who == nil { fmt.Printf("Nobody to say hi.") return } for _, people := range who { fmt.Printf("%s, %s\n", perfix, people) } }
- 可变参数必须在函数参数列表的最后一个(否则会引起编译时歧义);
- 可变参数在函数内部是作为切片来解析的;
- 可变参数可以不填,不填时函数内部当成 nil 切片处理;
- 可变参数可以填入切片;
- 可变参数必须是相同类型的(如果需要是不同类型的可以定义为 interface{}类型);
-
new函数
func new(Type) *Type
在Go语言中,new函数用于动态地分配内存,返回一个指向新分配的零值的指针,而不会初始化该内存。
-
声明不定长数组
a := [...]int{1, 2, 3, 4} a := [...]int{1: 20, 999: 10}
-
init函数
Go语言提供了先于main函数执行的init函数,初始化每个包后会自动执行init函数,每个包中可以有多个init函数,每个包中的源文件中也可以有多个init函数,加载顺序如下:
从当前包开始,如果当前包包含多个依赖包,则先初始化依赖包,层层递归初始化各个包,在每一个包中,按照源文件的字典序从前往后执行,每一个源文件中,优先初始化常量、变量,最后初始化init函数,当出现多个init函数时,则按照顺序从前往后依次执行,每一个包完成加载后,递归返回,最后在初始化当前包!
-
忽略导包
Go语言在设计时有代码洁癖,在设计上尽可能避免代码滥用,所以Go语言的导包必须要使用,如果导包了但是没有使用的话就会产生编译错误,但有些场景我们会遇到只想导包,但是不使用的情况,比如上文提到的init函数,我们只想初始化包里的init函数,但是不会使用包内的任何方法,这时就可以使用 _ 操作符号重命名导入一个不使用的包
import _ "net/http/pprof" import _ "github.com/go-sql-driver/mysql"
-
忽略字段
有的时候我们需要要使用方法中的某些参数时,我们可以使用下划线来进行进行忽略,避免Go编译时出错。
v1, v2, _ := function(...)
-
类型断言
我们通常都会使用interface,一种是带方法的interface,一种是空的interface,Go1.18之前是没有泛型的,所以我们可以用空的interface{}来作为一种伪泛型使用,当我们使用到空的interface{}作为入参或返回值时,就会使用到类型断言,来获取我们所需要的类型。
var b interface{} = 10 value, ok := b.(int64)
-
for range循环
切片/数组是我们经常使用的操作,在Go语言中提供了for range语法来快速迭代对象,数组、切片、字符串、map、channel等等都可以进行遍历,总结起来总共有三种方式:
// 方式一:只遍历不关心数据,适用于切片、数组、字符串、map、channel for range T {} // 方式二:遍历获取索引或数组,切片,数组、字符串就是索引,map就是key,channel就是数据 for key := range T{} // 方式三:遍历获取索引和数据,适用于切片、数组、字符串,第一个参数就是索引,第二个参数就是对应的元素值,map 第一个参数就是key,第二个参数就是对应的值; for key, value := range T{}
-
判断map的key是否存在
Go语言提供语法 value, ok := m[key]来判断map中的key是否存在,如果存在就会返回key所对应的值,不存在就会返回空值:
import "fmt" func main() { dict := map[string]int{"asong": 1} if value, ok := dict["asong"]; ok { fmt.Printf(value) } else { fmt.Println("key:asong不存在") } }
结尾的碎碎念
我正在筹备一个具有挑战性且对社会有深远意义的创业项目,现诚邀一位有责任心且具备运营能力的伙伴加入。如果你同样热爱挑战,追求有意义的事业,并擅长运营,欢迎联系我。让我们携手,一起为改变世界贡献力量!