const (_ = 1 << (10 * iota)KiB // 1024MiB // 1048576GiB // 1073741824TiB // 1099511627776 (exceeds 1 << 32)PiB // 1125899906842624EiB // 1152921504606846976ZiB // 1180591620717411303424 (exceeds 1 << 64)YiB // 1208925819614629174706176
)
Go中的Switch与其他语言有所不同,主要区别在于case后默认跟进一个break,如果想到达到其他语言的case效果,即case匹配之后后续case也执行,需要在case后接fallthrough关键字。
如今高并发已经是大型程序的标配,GO语言的火热一方面也是因为可以很方便的支持高并发。在Go中有两种实现并发的方式,接下来我们一一讨论。
协程和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是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的方法实现广播的效果,从而实现一对多的协程通信。
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 lockedmutexWokenmutexStarvingmutexWaiterShift = iotastarvationThresholdNs = 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 int64starving := falseawoke := falseiter := 0old := m.statefor {// 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.statecontinue}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 != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif 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 == 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 = trueiter = 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.staterace.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 := newfor {// 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<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进行赋值,需要先进行一次强转。
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的底层结构是一个数组。在Go语言中,数组的长度是不可变的,而slice是基于数组的包装,可以实现长度的变化。
slice内部维护的数组长度对应着slice的容量,容量可以在使用make函数初始化slice时进行指定,当slice的容量装满时再添加元素,slice会进行扩容,扩容的过程是再申请一块更大的数组(通常为原数组长度二倍),随后将原数组中的左右元素逐个拷贝到新数组中。扩容耗时较大,初始化时合理的指定slice的容量可以提高性能。同时slice内部还维护着一个长度,长度代表底层数组已经被使用的数量。也就是说,一个slice其实是一个包含一个指向底层数组的指针,数组长度(容量)和长度的结构体。
由于slice是引用类型的变量,对slice的普通赋值(浅拷贝)只是复制了一个底层数组的指针,通过该指针可以修改同一个底层数组,因此向函数中传递slice是可以直接修改其值的。
slice的删除是比较复杂的,如果不追求顺序,可以使用最后一个元素替换要删除的元素,同时将slice的长度减一。
面向对象通常意味着三种性质:封装性,继承性和多态性。在GO语言中,结构体可以实现封装性和继承性,与其语言中类的概念不同的是,结构体无法实现多态性。多态性可以由GO语言中的接口类型实现。
GO语言中的函数也可以视为一种变量,函数的类型(也叫函数签名)由函数的形参类型和返回值类型决定,可以将形参和返回值类型看做函数的变量类型,而函数内部的代码看做函数的值。函数值之间是不可比较的,因此也不可以作为map的key。
函数的返回值也可以取变量名。当一个函数的所有返回值都具有变量名时,可以在函数运行的过程中直接使用变量名,最终可以省略return语句。
有名称的函数只能在包级别中定义,但在一份函数内部可以定义其他的匿名函数。与匿名结构体类型,匿名函数就是先写出完整的函数类型,再写出函数内的代码,也就是函数值。匿名函数可以访问外层函数的所有变量。
与其他语言不同,GO函数使用的是可变栈,因此不会因为栈的大小限制函数的递归深度。
GO语言中不支持函数重载,但提供了一个可变参数。可变参数在使用时有以下几个注意事项:
defer是GO语言提供的一个独特的控制关键字,该关键字的作用是在函数结束时执行后面的语句,这里的结束具体指的是计算完返回值。在处理一些必须在函数返回前执行的操作非常有用。例如释放网络资源(resp.Body.Close),释放IO,(file.Close),释放锁(RWLock.Unlock),计算函数运行时间等等。当然为了追求更高的性能,上述操作都可以在函数执行过程中挑选合适的时机执行。
defer有两个特点,1是函数无论以何种形式返回都会执行defer,甚至包括panic,2是如果有多个defer语句则会从下向上执行(与定义的顺序相反)。
与其他语言一样,程序如果在运行时出现不可预知的严重错误(例如数组越界,空指针等)或者手动调用panic时会抛出异常,如果异常没有被处理则会导致程序结束运行。在某些情况下,即使某些子程序崩溃也不影响程序主体继续运行,此时我们就要让程序从异常中恢复。
例如一个用GO编写的web后台服务器可能对外提供多个API,当某个用户访问一个API并发生异常时,只需要终止该次访问即可,主程序应该继续提供其他服务。
由于defer后的语句即使在函数panic之后也会继续运行,因此recover操作需要在defer之后执行。使用方法是调用recover函数recover(),当没有panic发生时,recover()会返回nil,此时函数正常返回。当recover()返回非nil时,说明函数发生了panic,我们需要向上层提供错误信息。
这些操作不是一行能写完的,因此使用recover需要将其放在defer后的匿名函数中。
与其他语言不同,GO使用错误来处理程序在运行期间发生的异常,对于许多库函数都会用一个额外的返回值来表示运行期间是否发生错误。错误error是一个接口,里面包含了错误信息。
在其他语言中,函数和方法并没有设么本质区别,只是称呼不同,但在GO语言中却不是这样。准确来说,GO中的方法类似于其他语言的类的成员函数。
与其他语言不同的有以下几点:
package maintype 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.RWMutexcache 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语言实现了多态性(最典型的例如fmt.Println(format string, …arg interface{})),通过接口可以让函数接受多种多样的变量类型。同时也实现了一定程度的封装性,当变量被转换为接口类型时,便只对外提供指定的方法,其他方法和所有变量都不可见。
sort是常用接口之一,这里以他为例说明GO接口的使用方式。
type AB struct {A intB 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)}
}
首先需要明确的一点是,开发规范并不是一个固定的东西,它只是为了代码之后的可维护性和可读性而由程序员整理出来的一些规范。但具体到每个人身上每个人可能都有自己的一套规范。对于一个小项目来说,开发人员可能只有一两个人,这时开发规范的作用几乎可以忽略。但一个项目想要做大,一个合理的规范是必不可少的,不然代码只能成为一坨屎山,会大大增加后续的开发和维护成本。
由于本人也只是一个互联网小白,这里我选择站在巨人的肩膀上,使用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层
RESTful风格简单来说就是将一个URI视作一个资源,使用HTTP中的GET,POST,PUT与DELETE方法与Header中的字段来标识操作动作。
r.GET(URI, HandleFunc)
r.POST(URI, HandleFunc)
r.PUT(URI, HandleFunc)
r.DELETE(URI, HandleFunc)
context.Json(code, data) // data可以使任意结构类型的对象
context.Query(key)
context.GetQuery(key)
context.DefaultQuery(key)
context.PostForm()
context.GetPostForm()
context.DefaultPostForm()
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()
上一篇: 分享一款“东阳麻将可以开挂”!(其实是有挂)-知乎
下一篇:英国央行如期按兵不动