Go圣经学习笔记
- while:Go语言中没有while语句,但其for语句十分灵活,可以省略任意一项。当init和post部分都省略时,for condition {} 就成了while语句。
- map value初始化:如果对map使用不存在的key去变量不会报错,而是会取到对应类型的零值类型,例如string的空字符串,数值类型的0,集合类型的nil。因此在利用map进行类似于统计key出现次数的功能时,可以直接使用 map[key]++ ,而不用单独初始化。
- 如果要判断一个value是否存在map中,可以使用两个参数接受map返回的值,value, ok := map[key],当key存在时ok返回true,否则返回false。
- 类似的,Channel也可以通过该方法判断是否关闭,value, ok := ← chan,当ok为false时代表Channel已经关闭。
- map中value的地址可能会随着map的扩容而改变,因此禁止对value取地址。
- GO语言中没有set结构,可以用value值没有意义的map来实现set的功能,此时value可以是bool类型,还可以是空结构体struct{}类型。空结构体类型占据的内存更小,但写法稍微复杂。
- GO中有一个比较特殊的目录:internal。在internal中的包可以被其父路径上的其他包所调用,但除此之外的路径时无法访问的。internal中通常用来存程序的关键代码,这些代码不对外公开。internal中还有一个比较特殊的目录:pkg。pkg中的包不受internal约束,是可以被外界访问的,通常用来保存一些通用的工具函数。
常量
- 常量会在编译期确定,之后便不再更改,因此只能是基本数据类型。
- 如果没有显性的指出常量的类型,则会根据右边的数据来推断常量的类型,并使用比基础类型精度更高的结构进行存储,至少256bit。如果数值相等,常量可以直接赋值给其他类型的变量,例如 const a = 0.0 var b int = a 是合理的。
- 枚举(iota生成器):go中没有枚举类型,但可以利用常量加iota生成器实现类似于枚举的效果。iota关键字的作用范围是一组常量,在声明处值为0,同时会将对应的表达式应用到当前常量组中的所有常量,并且每次加1。
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
Switch
Go中的Switch与其他语言有所不同,主要区别在于case后默认跟进一个break,如果想到达到其他语言的case效果,即case匹配之后后续case也执行,需要在case后接fallthrough关键字。
并发
如今高并发已经是大型程序的标配,GO语言的火热一方面也是因为可以很方便的支持高并发。在Go中有两种实现并发的方式,接下来我们一一讨论。
Goroutine和Channel
协程和Channel是go语言支持高并发的精髓。其中协程负责并发执行任务,Channel负责在协程之间传递信息,并且可以对协程进行控制。
按照容量大小可以将Channel分为阻塞性Channel和非阻塞性Channel,二者的区别在与定义时是否具有第二个参数:
var blockChan = make(chan int)
var unblockChan = make(chan int, 10) // 第二个参数10即为Channel的最大容量,当Channel内的数据达到容量之后,发送操作同样会阻塞。
在实现高并发时,我们通常需要根据实际情况定义合适容量的非阻塞Channel,阻塞Channel通常只用来实现协程之间的通信与同步。
Channel可以直接调用内置的close方法进行关闭,向一个被关闭的Channel内写入数据会panic,但读取数据是可以的,读取会一直返回零值。如果想要确定一个Channel是否会关闭,可以使用如下方法:
var intChan = make(chan int, 10)
close(intChan) // 关闭chan
if i, ok := intChan; ok { // 当channel被关闭之后,ok为false。这种方式在GO中被多出运用。
...
}
在实际应用时,一个协程通常只会往Channel中发送或从其中接受,一般不会出现既发送又接受的情况,因此GO提供了在函数形参处定义单方向Channel的方法。当一个Channel被定义为 chan<- int时,在对应的函数中只能向Channel中发送数据。当定义为←chan int时,则对应函数只能从Channel中接受数据。需要注意的是,单方向Channel只是GO提供的一种包装类型,底层是一样的,Channel在函数中的应用是否合法会在编译期检测。
带缓存的Channel在用法上与不带缓存的一样,只是在发送数据时如果缓存未满不会发生阻塞。不带缓存的Channel在本质上就是一个缓存大小为0的Channel。同时Channel也可以使用range关键字进行迭代。
Select多路复用
select是Go提供的一个独特的关键字,select的用法与switch类似,区别在于select之后没有参数,参数放在具体的case之后,例子如下:
select {
case: <- chan1:
// do something
case: x := <- chan2:
// do something
default:
// do something
}
select的具体作用是在任何一个case可以执行时即去执行其后面的语句,通常与for循环一起运用。当同时有多个case语句可以执行是,则会随机选择执行。
当分支中不存在default时,如果没有可以执行的case,select语句会一直阻塞。但有default分支,当没有任何可以执行的case时(即case全部阻塞时)会立即执行default后的语句。
这里有一个小技巧,由于向值为nil的Channel发送和接受都会一直阻塞,因此可以将select中的某些case语句中的chan值置1来禁用一些case。
还有一个小技巧,由于从一个关闭的Channel获取的第二个值会是false,因此可以使用关闭Channel的方法实现广播的效果,从而实现一对多的协程通信。
Goroutine和锁
Channel是GO提供的一个独特的用于并发的数据结构,使用Channel可以实现代码的无锁化,提高程序性能,最主要的是,这很GO!
但并不是所有问题都可以用Channel简单实现的,在某些情况下,还是会出现多个协程共享一个变量的情况,此时还是要使用锁。只要操作的数大于一个机器字(32位机器为4字节,64位为8字节),在任何情况下都要避免竞争。
使用Channel和锁不仅能避免竞争,同时还能避免因为CPU乱序造成的一些不可预期的bug,这种bug不会稳定触发,且极难debug。因此,一个优秀的程序员,在任何时候都不应该让一个变量不经过任何处理在多个协程之间并发读写。
GO中内置了两种锁,分别是读写锁和互斥锁,都在sync包中。互斥锁的底层可以用一个容量为1的Channel,同时最多只能有一个协程获取Channel中的值。
无论是互斥锁还是读写锁的使用都很方便,无非就是获取数据前调用lock方法,使用完数据后调用unlock方法,这里我主要研究一下其底层实现。
互斥锁
首先是相对简单的互斥锁,其所有相关代码如下图所示,即使相对简单,其实现代码也200多行了:
type Mutex struct {
state int32 // 锁的核心,表示锁的状态
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
// 从代码中可以看到,加锁步骤实际上是分两步的,分别较fast Path和slow Path。
// 当接收到加锁命令时,Mutex首先会使用更为底层的同步方法尝试修改m.state的值,也就是直接修改锁的状态,修改成功代表加锁成功,直接返回。如果修改失败代表锁被占用,进入slow path
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
//
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// Don't spin in starvation mode, ownership is handed off to waiters
// so we won't be able to acquire the mutex anyway.
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
// Try to set mutexWoken flag to inform Unlock
// to not wake other blocked goroutines.
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
if old&mutexStarving == 0 {
new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode.
// Critical to do it here and consider wait time.
// Starvation mode is so inefficient, that two goroutines
// can go lock-step infinitely once they switch mutex
// to starvation mode.
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
runtime_Semrelease(&m.sema, true, 1)
}
}
变量逃逸
一个函数内的基本类型的局部变量通常是分配在栈内存上的,他会随着函数的结束而被销毁。但在某些情况下变量会被分配在堆内存上,在函数返回之后仍然可以访问,在GO中称为变量逃逸。发生变量逃逸的条件有:变量被全局变量引用,变量是引用类型。变量逃逸会降低程序的性能,逃逸的变量会被GO的垃圾回收销毁。
类型重定义
GO可以使用type关键字将一个类型封装成另外的类型,例如 type temperature float64。此时temperature类型底层仍然是一个float64,但它可以拥有自己的方法,且不能直接使用float64进行赋值,需要先进行一次强转。
Rune,Byte和String
String可以与[]byte类型相互转换,其底层数据是一样的。rune代表一个utf8字符,底层数据结构与int32相同,可能包含多个byte数据。
以下两种for循环遍历的结果是不同的,在使用for range遍历时go会自动按照编码将字符串切割为rune。
func test() {
ss := "腾讯qq"
for i, s := range ss {
fmt.Println(i, reflect.TypeOf(s)) // 0, 3, 6, 7, int32(rune)
}
for i := 0; i < len(ss); i++ {
fmt.Println(i, reflect.TypeOf(ss[i])) // 0, 1, 2, 3, 4, 5, 6, 7 int8(byte)
}
}
字符串字面量
使用``符号包含的字符串会忽视所有转义字符,可以十分方便的编写正则表达式。
&^位清空
z := x &^ y // 将x中y为1的位置置零
Slice
底层结构:slice的底层结构是一个数组。在Go语言中,数组的长度是不可变的,而slice是基于数组的包装,可以实现长度的变化。
slice内部维护的数组长度对应着slice的容量,容量可以在使用make函数初始化slice时进行指定,当slice的容量装满时再添加元素,slice会进行扩容,扩容的过程是再申请一块更大的数组(通常为原数组长度二倍),随后将原数组中的左右元素逐个拷贝到新数组中。扩容耗时较大,初始化时合理的指定slice的容量可以提高性能。同时slice内部还维护着一个长度,长度代表底层数组已经被使用的数量。也就是说,一个slice其实是一个包含一个指向底层数组的指针,数组长度(容量)和长度的结构体。
由于slice是引用类型的变量,对slice的普通赋值(浅拷贝)只是复制了一个底层数组的指针,通过该指针可以修改同一个底层数组,因此向函数中传递slice是可以直接修改其值的。
slice的删除是比较复杂的,如果不追求顺序,可以使用最后一个元素替换要删除的元素,同时将slice的长度减一。
结构体
面向对象
面向对象通常意味着三种性质:封装性,继承性和多态性。在GO语言中,结构体可以实现封装性和继承性,与其语言中类的概念不同的是,结构体无法实现多态性。多态性可以由GO语言中的接口类型实现。
- 结构体的嵌入。结构体的概念与其他语言的类有些类似,但又不完全相同。首先结构体没有继承的概念,但有着与继承类似的嵌入关系。即一个结构体中可以包含另一个结构体。
- 嵌入的意义:继承或包含。嵌入通常有两种意义,在一些情况下,嵌入的结构体类似于其他语言的父类。因为嵌入关系没有限制,因此一个结构体可以嵌入多个不同类型的其他结构体,这类似于多继承,是其他语言无法实现的。但在更多的情况下,嵌入只是单纯的表示包含关系,例如一个圆可以由圆心和半径表示,那么一个圆的结构体就可以由一个点结构体和一个长度确定,点和圆之间是包含关系,但却不是继承。
- 匿名成员:在嵌入其他类型结构体时,可以存在只声明类型而不声明变量名的情况,此时称为嵌入了一个匿名成员。外层结构体可以像访问自身的变量一样只使用 . 运算符访问嵌入的内层结构体的变量,同时也可以访问其方法。就像搭积木一样,可以使用匿名成员将各种各样的小结构体拼装成一个大结构体。
- 封装性:结构体中可以通过首字母的大小写来控制字段或方法能否被其他包访问,也就是封装性。但这里需要注意的是,GO最小的封装单元是包,也就是说无论结构体中的字段首字母是大写还是小写,在包内都是可以被其他函数和对象随意访问的,这点与其他语言有很大不同,也就是说不存在所谓的私有变量。如果将包级别与类级别做类比,首字母大写对应 public,可以被随意访问,首字母小写对应protect,在包内(类以及其子类)中随意访问,不存在private(不存在只能在结构体内访问的类型)。
函数
GO语言中的函数也可以视为一种变量,函数的类型(也叫函数签名)由函数的形参类型和返回值类型决定,可以将形参和返回值类型看做函数的变量类型,而函数内部的代码看做函数的值。函数值之间是不可比较的,因此也不可以作为map的key。
函数的返回值也可以取变量名。当一个函数的所有返回值都具有变量名时,可以在函数运行的过程中直接使用变量名,最终可以省略return语句。
匿名函数
有名称的函数只能在包级别中定义,但在一份函数内部可以定义其他的匿名函数。与匿名结构体类型,匿名函数就是先写出完整的函数类型,再写出函数内的代码,也就是函数值。匿名函数可以访问外层函数的所有变量。
递归
与其他语言不同,GO函数使用的是可变栈,因此不会因为栈的大小限制函数的递归深度。
可变参数
GO语言中不支持函数重载,但提供了一个可变参数。可变参数在使用时有以下几个注意事项:
- 可变参数必须是最后一个形参。
- 因为只是最后一个参数,因此可变参数只能指定一种类型。
- 在函数内部使用时,可变参数相当于一个slice,可以通过for循环进行遍历。
- 可变参数使用…变量类型表示,例如:…int, …string, …interface{}。
- …符号跟在slice之后代表将当前slice中的所有元素逐个取出放到可变参数中。
Defer
defer是GO语言提供的一个独特的控制关键字,该关键字的作用是在函数结束时执行后面的语句,这里的结束具体指的是计算完返回值。在处理一些必须在函数返回前执行的操作非常有用。例如释放网络资源(resp.Body.Close),释放IO,(file.Close),释放锁(RWLock.Unlock),计算函数运行时间等等。当然为了追求更高的性能,上述操作都可以在函数执行过程中挑选合适的时机执行。
defer有两个特点,1是函数无论以何种形式返回都会执行defer,甚至包括panic,2是如果有多个defer语句则会从下向上执行(与定义的顺序相反)。
Recover
与其他语言一样,程序如果在运行时出现不可预知的严重错误(例如数组越界,空指针等)或者手动调用panic时会抛出异常,如果异常没有被处理则会导致程序结束运行。在某些情况下,即使某些子程序崩溃也不影响程序主体继续运行,此时我们就要让程序从异常中恢复。
例如一个用GO编写的web后台服务器可能对外提供多个API,当某个用户访问一个API并发生异常时,只需要终止该次访问即可,主程序应该继续提供其他服务。
由于defer后的语句即使在函数panic之后也会继续运行,因此recover操作需要在defer之后执行。使用方法是调用recover函数recover(),当没有panic发生时,recover()会返回nil,此时函数正常返回。当recover()返回非nil时,说明函数发生了panic,我们需要向上层提供错误信息。
这些操作不是一行能写完的,因此使用recover需要将其放在defer后的匿名函数中。
错误
与其他语言不同,GO使用错误来处理程序在运行期间发生的异常,对于许多库函数都会用一个额外的返回值来表示运行期间是否发生错误。错误error是一个接口,里面包含了错误信息。
处理策略
- 传播错误。当一个子程序发生错误时,最常见的做法是将错误向上传播,并添加当前已知原因和其他参数的详细说明。
- 重试。当错误的原因比较偶然时,可以在原地尝试重试。但要注意重试的时间间隔和次数,避免死循环。
- 结束程序。如果错误比较严重以至于程序无法继续运行,可以打印信息并终止程序。此时错误应该称为异常,可以直接调用panic。需要注意的是,该策略一般只能在main函数中进行,不应该给其他函数赋予如此大的权力。
- 打印错误信息。如果一个错误影响较小,可以在原地直接打印错误信息,并继续运行。
- 忽略错误。通常用于已知的且不会造成实际影响的错误
方法
在其他语言中,函数和方法并没有设么本质区别,只是称呼不同,但在GO语言中却不是这样。准确来说,GO中的方法类似于其他语言的类的成员函数。
与其他语言不同的有以下几点:
- 成员函数一般都要定义在类中,而GO的方法可以在任意处定义,甚至可以跨文件,只需要在函数名之前添加其所属的类型。
- GO中的方法不使用默认的self或this指针表示自身,而是可以自定义指向自身的接收器的名称。
- GO可以为任意的变量类型设置方法,不局限于结构体。同时由于GO还可以通过相同的底层数据定义不同的变量类型,二者结合可以实现万物均有方法。
- 方法的接收器可以是类型本身或者是一个类型对应的指针,二者在使用上是没有任何区别的。调用任何方法时,会在调用处生成一个对应对象的拷贝,这使得类型与指针作为接收器会产生一些区别。
- 类型接收器和类型指针接受器的区别有以下两点:首先,如果结构体本身较大,频繁的拷贝可能会降低性能,因此对于较大的结构体建议将接收器定义成指针类型。
- 其次,由于实际调用方法的是对象的拷贝,当方法的接收器为类型本身时,实际上是一个对象的拷贝在调用方法,方法中的任何操作不会影响原来的对象。而当接收器为类型指针时,实际上是指针的拷贝在调用方法,而指针所指向的对象就是原对象,任何修改都会被记录在原对象中。下面的例子便反应了这一点。如果我们想实现一个记录自身被调用几次的方法,那么接收器一定要是指针类型。
package main
type A struct {
a int
}
func (a A) Add() {
a.a++
}
func (a *A) PAdd() {
a.a++
}
func main() {
aa := A{a: 1}
aa.Add() // aa.a还是1,对象aa没有发生任何改变,改变的只是aa的拷贝
aa.PAdd() // aa.a变为2,影响的就是aa对象
}
之前我们了解了GO中的结构体嵌入,以及结构体中的匿名成员。结构体可以直接访问匿名成员的变量,同样也可以直接调用匿名成员的方法,就像调用其本身的一样。
线程安全的类型
GO语言的默认类型都不是线程安全的,但通过类型定义和内嵌成员变量的方法,我们可以自定义一些十分方便的数结构,下面的例子中我分别定义了一个简单的线程安全Map类型。需要注意的是,这里的接收器要是指针类型,否则可能有未知错误。
type syncMap struct {
sync.RWMutex
cache map[int]string
}
func (m *syncMap) Init() {
m.cache = make(map[int]string)
}
func (m *syncMap) Add(key int, value string) {
m.Lock()
defer m.Unlock()
m.cache[key] = value
}
func (m *syncMap) Get(key int) (value string) {
m.RLock()
defer m.RUnlock()
return m.cache[key]
}
func (m syncMap) Del(key int) {
m.Lock()
defer m.Unlock()
delete(m.cache, key)
}
与函数一样,方法也是一个变量,接收器、方法的形参和返回值类型时方法的类型,内部代码就是方法值。可以像正常变量一样为其他变量赋值,并可以像函数一样对赋值之后的变量进行调用。
接口
在GO语言中,面向对象中的多态性就是用接口实现的。GO语言的接口与其他语言有相似之处,但又存在许多不同:
相同点:接口都是抽象类型,接口类型只包含方法,不能有任何变量。一个具体类型如果想实现接口就需要实现接口内的所有方法。
不同点:
- GO中的结构体如果想要实现接口不需要显示继承任何信息,只要实现了接口对应的方法系统就会认定该结构体实现了对应的接口。
- 由于接口的实现是隐式的,因此甚至可以先编写一些有共同方法的结构体,再根据这些结构体从其中提取出一个接口,而不用对结构体做任何修改。
- 由于没有父类的概念,GO没有办法使用结构体实现多态性,但可以通过接口实现。如果一个对象被强转为对应的接口类型,那么他只会对外暴露接口中的方法,并且不会再暴露任何内部对象。
- 与结构体类似,接口同样可以内嵌,通过小接口组合成大接口。
特殊的接口类型:interface{}(空接口)。空接口中没有任何方法,因此所有类型都可以视为实现了interface{}接口。类似于Java中的object,这里也体现了GO使用接口实现多态性的特点。
接口实际上只是一个称呼,我们实际使用的其实都是实现了指定方法的实际对象。可以将接口视为帮助我们组织代码的一种约定。
通过接口,GO语言实现了多态性(最典型的例如fmt.Println(format string, …arg interface{})),通过接口可以让函数接受多种多样的变量类型。同时也实现了一定程度的封装性,当变量被转换为接口类型时,便只对外提供指定的方法,其他方法和所有变量都不可见。
sort接口
sort是常用接口之一,这里以他为例说明GO接口的使用方式。
type AB struct {
A int
B string
}
type ABSortByA []AB // 定义一个底层数据类型相同的类型,用来实现接口方法
// sort接口共有三个方法,Len, Swap, Less
func (a ABSortByA) Len() int {
return len(a)
}
// Swap函数一般没什么可以修改的,除非是对结构体内部分排序
func (a ABSortByA) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
// 通过修改Less方法可以按照自定义条件排序,例如这里实现的就是按照A升序排序
func (a ABSortByA) Less(i, j int) bool {
return a[i].A < a[j].A
}
func main() {
ab := []AB{{A: 2, B: "aa"}, {A: 1, B: "bb"}, {A: 3, B: "cc"}}
sort.Sort(ABSortByA(ab)) // 调用sort.Sort函数实现排序,排序钱需要将对象转为实现对应接口的类型,底层数据其实是一样的
fmt.Println(ab) // [{1 bb} {2 aa} {3 cc}]
}
类型断言
Go语言中的多态性是由接口来提供的,如果想将转为接口的结构体类型重新变为结构体,则需要使用类型断言。类型断言的语法有两种,由于第一种有发生panic的风险,在实际使用中要尽量避免,尽量使用第二种类型断言方式。
var common interface{}
var cStr string
var cInt int
common = "123"
// 第一种,只有一个返回值
cStr = common.(string) // 只有一个返回值的类型断言
cInt = common.(int) // 当接口类型无法转为对应的实际类型时,程序会panic,慎用
// 第二种,有两个返回值
cStr, ok = common.(string) // 第一个返回值是实际值,第二个表示是否可以转化
cInt, ok = common.(int) // 转换失败ok为false,不会panic,值为对应的零值
除了结构体之外,类型断言还可以将一个对象转换为接口类型,通常用来判断一个对象是否具有某种方法。
类型分支
除了可以使用类型断言来将一个接口转为具体类型,还可以通过类型分支来判断一个接口是哪种类型,在输入的接口可能是多种类型并且每种类型的处理方式不同时尤其有用,相关代码如下:
// typePrint 类型分支实例
func typePrint(x interface{}) {
switch x := x.(type) { // 在不需要使用x的具体值时,前面的x可以省略
case int: // case选择是x.(type)的返回值,也就是x的类型
fmt.Printf("int: %d\n", x)
case bool:
if x {
fmt.Printf("bool: %s\n", "TRUE")
} else {
fmt.Printf("bool: %s\n", "FALSE")
}
case string:
fmt.Printf("string: %s\n", x)
default: // default用来接收未定义类型
fmt.Printf("unknow type, %v\n", x)
}
}
Go开发规范
概述
首先需要明确的一点是,开发规范并不是一个固定的东西,它只是为了代码之后的可维护性和可读性而由程序员整理出来的一些规范。但具体到每个人身上每个人可能都有自己的一套规范。对于一个小项目来说,开发人员可能只有一两个人,这时开发规范的作用几乎可以忽略。但一个项目想要做大,一个合理的规范是必不可少的,不然代码只能成为一坨屎山,会大大增加后续的开发和维护成本。
由于本人也只是一个互联网小白,这里我选择站在巨人的肩膀上,使用GoFrame框架所定义的规范。
规范
工程目录结构
实际工程并不需要一一对应,可根据工程实际情况增删文件夹。
/
├── api // 对外提供的api接口,相当于control层。
├── internal // 项目主要代码部分,internal文件夹是Go1.4版本之后提供的功能,里面的所有文件都不可以被外部访问,提高了安全性
│ ├── cmd // 命令行管理目录,可以维护多个命令行
│ ├── consts // 常量目录,负责定义项目用到的所有常量
│ ├── handler // 接口目录,接受并解析用户输入参数的入口层
│ ├── model // 模块层,负责定义所有输入输出的数据结构
│ │ └── entity // 维护数据与集合一一对应的数据模型,由工具管理
│ └── service // 业务逻辑代码
│ └── internal
│ ├── dao // 和数据库交互的代码,只包含基本的CURD功能
│ └── dto // 业务模型到数据模型的转换,由工具维护
├── manifest // 程序编译,运行,交付和配置等相关文件
│ ├── config // 配置文件
│ ├── docker // docker相关文件
│ └── deploy // 部署相关文件
├── resource // 静态资源,可以在编译阶段添加到程序中
├── utility
├── go.mod // go中的包管理文件
└── main.go // 程序入口
结构化约束
即万物皆结构体,任何在包之间传递的数据都需要用结构体来表示,即使只有一个参数。
结构体的命名规范为:业务名+分层名+任务名+请求/响应
例如一个service层传递给DAO层的数据结构可以是:
type UserServiceGetUsernameReq {
user_id string
}
type UserServiceGetUsernameRes {
username string
}
结构体转换
可以为结构体定义Parse方法和GetStruct方法用于将其他数据结构转换为当前数据结构
数据校验
开发时要时刻牢记,客户端传递的数据是不可信的,必须进行参数校验,go语言中可以通过给结构体绑定 v 标签自动完成校验。
v标签的常见用法:
required:必须存在的字段
length:指定字段长度限制
same: 指定字段与其他字段相同
min: 指定字段最小值
type UserApiLoginReq {
g.Meta `path:"/user/sign-in" method:"post" tags:"User" summary:"Sign in with exist account"` // GoFrame中的特殊字段,相当于Controller方法
Username string `v:required#username cannot be null`
Password string `v:required#password cannot be null|length:6,16`
Password2 string `v:required#password2 cannot be null|length:6,16|same:Password`
}
GoFrame中,通过相同的名称接受字段,通过v标签校验字段,通过r.Parse方法转换结构。
分层
控制层(api层):负责接受、转换、校验、处理请求参数,并将参数传递给service层
Gin框架学习笔记
RESTful
RESTful风格简单来说就是将一个URI视作一个资源,使用HTTP中的GET,POST,PUT与DELETE方法与Header中的字段来标识操作动作。
RESTful风格
r.GET(URI, HandleFunc)
r.POST(URI, HandleFunc)
r.PUT(URI, HandleFunc)
r.DELETE(URI, HandleFunc)
发送Json
context.Json(code, data) // data可以使任意结构类型的对象
获取GET数据
context.Query(key)
context.GetQuery(key)
context.DefaultQuery(key)
获取POST数据
context.PostForm()
context.GetPostForm()
context.DefaultPostForm()
获取URI参数
r.Get(“/:name/:age”, login)
context.Param(key)
参数绑定
利用获取到的参数自动生成结构体
对应的结构体需要用form:name
类型的tag进行标记
context.ShouldBind(&struct)
POST与GET方法均可使用
从请求中获取文件并保存
context.FormFile(file)
context.SaveUploadFile(file, path)
重定向
context.Redirect(code, new_url) // 外部重定向
转发
context.Request.URL.Path = new_uri
router.HandleContext(context)
router.NoRoute() // 用于接收未定义的URI
路由组
group := router.Group(groupName)
group.GET(uri, handleFunc)
路由组是支持嵌套的
中间件
gin中的中间件函数就是一个输入参数为*gin.Context类型的函数
通过r.Use(middleWareFunc)进行添加
c.Next() // 继续执行其他处理函数
c.Abort() // 组织调用后续的处理函数
应用中间件时,一般使用闭包的方式返回一个中间件函数,这样方便在外围进行一些额外的处理。
func getMiddleWareFunc(args) gin.HandlerFunc {
return func() *gin.Context {}
}
中间件可以为路由组注册,也可以为单个路由注册,通过在路由方法中串行加入
跨路由存取
context.Set()
context.Get()
需要注意
由于gin路由中使用的都是指针类型的*gin.Context,如果想要新开一个协程处理Context,要考虑线程安全,尽量传入context.Copy()