開始之前,介紹一下?最近很火的開源技術,低代碼。
作為一種軟件開發技術逐漸進入了人們的視角里,它利用自身獨特的優勢占領市場一角——讓使用者可以通過可視化的方式,以更少的編碼,更快速地構建和交付應用軟件,極大程度地降低了軟件的開發、配置、部署和培訓成本。
應用地址: https://www.jnpfsoft.com
開發語言:Java/.net
這是一個基于 Java Boot/.Net Core 構建的簡單、跨平臺快速開發框架。前后端封裝了上千個常用類,方便擴展;采用微服務、前后端分離架構,集成了代碼生成器,支持前后端業務代碼生成,滿足快速開發;框架集成了表單、報表、圖表、大屏等各種常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3,平臺即可私有化部署,也支持 K8S 部署。
從其他語言剛轉入go語言的時候比較容易出現以下方面的問題:
- 字符串string
- interface斷言
- 切片slices
- map
- 控制結構(for、switch)
- defer channel管道
- sync同步機制
- select+timer
1.字符串String(Split分割)
項目可能使用情況:?當使用string的split功能分割空字符串時,再進行數據庫模糊查詢時候,如下:
踩坑分析:?當對空字符串進行Split,將會返回一個包含一個空字符串的切片數組,數組長度為1,但是查詢時由于空字符串會被過濾掉了該條件,會導致查詢出來的數據不正確,甚至可能會是全表掃,由于查詢所有數據可能會系統崩潰掉。
如何避坑:?使用前可以排除空字符串
2.interface斷言
在項目中也會經常使用類型斷言,當使用interface()轉化成相對應的類型時,如果不恰當使用斷言而導致panic,踩坑代碼:
踩坑分析:?golang中對于類型的斷言,一定需要加上第二個參數ok判斷,否則類型不一致的話直接panic退出?如何避坑:?增加第二個參數ok來判斷
3.切片slice
3.1 容量問題
要注意在make切片的時候的參數設置,參數設置有問題很容易導致取下標值不是自己想象中的值,如下:
踩坑分析:?一般來說,slice的初始化為 make([]T, length, capacity)。 如果省略了capacity,默認capacity等于length。因此上面建了一個[]int類型的切片,長度和容量為3的[0,0,0]切片,因此通過append(s,1)會使slice擴容成6,并添加元素1進去。輸出結果為:[0,0,0,1]
如何避坑:1.使用make([]T, length, capacity)補全參數;2.使用make([]T, length),則使用通過索引方式賦值,例如,s[0]=1
3.2?截取[:n]
在項目中可能會使用到切片截取功能,如下簡單的代碼,那么會出現什么問題呢?
踩坑分析:?因為切片的截取是引用關系,共有 2 個切片 a 和 b,截取了 a 的一部分賦值給了 b,兩者存在著關聯。圖3-2-1 因此,雖然切片 a 只有底層數組中 0 和 1 兩個索引位正在被使用,其余未使用的底層數組空間毫無作用,圖3-2-2。但由于正在被引用,他們也不會被 GC,因此造成了內存泄露。
圖3-2-1
圖3-2-2
如何避坑: 可以通過拷貝的方式,同時將原有的切片或者數組釋放。
4.map
4.1nil的map賦值問題
在項目中也經常使用到map,但是對于map的使用也很容易出錯,比如,對一個nil的map進行賦值:
踩坑分析:?對未初始化的map變量,添加元素時會空指針panic,拋出錯誤:
如何避坑:往map添加元素時需要先分配內存。 例如 m := make(map[int]int)
4.2?判斷map中的key是否存在
在使用map的key取值時,需要先判斷key是否存在,踩坑代碼:
如何避坑: 不能通過取出來的值來判斷key是否存在map中。需要采用如下的形式:
if _, ok := m[1]; !ok {print("key not exists")} ?
4.3map的遍歷順序問題
在使用map for循環時,也會出現一些踩坑問題,比如,判斷map兩次循環相同順序的值是否一致。
踩坑分析:?map的遍歷時,golang會提前取一個隨機數,把桶的遍歷順序隨機化。因此,在程序中,不能依賴遍歷的順序。?如何避坑: 如果需要確保遍歷順序,一般需要自行維護一個額外的有序的數據結構。比如,使用list+sorts
4.4?map的并發讀寫
在使用map時需要注意,map寫入和讀取操作是否存在并發問題,特別是引用第三方庫的時候,比較容易出現并發map操作的問題,比如:
踩坑分析:?golang中的普通map不是線程安全的,如果并發讀寫,會導致panic。出現這樣的錯誤:
如何避坑:不要并發讀寫map,也即不要在多個goroutine中同時對map進行讀和寫。如果一定要有讀和寫,可以使用sync.Map,但是sync.Map性能比較低,小心使用。
5.控制結構
5.1for循環取址問題
在項目中經常使用for循環進行遍歷,但是很容易在指針類型上使用錯誤,比如:
踩坑分析:?因為在循環里創建的所有函數變量共享相同的變量,其實就是一個可訪問的存儲位置,而不是固定的值。 因此在for多次循環中,value的地址只有一個。比如,在上面的循環變量p中,在每次迭代中只給它分配了一個新值,而循環變量的地址在每次迭代中都是相同的,因此將存儲相同的指針。因此,上面的遍歷中,在循環之后,它將保存在最后一次迭代中分配的值。因此運行以上代碼,輸出如下,和預期不一樣:
如何避坑:
(1).在上面的case中不要使用指針
(2).在本地賦予一個臨時指針,使用臨時指針進行賦值,就不會被覆蓋。
for _, p := range persons {innerP := ppersonMap[p.name] = &innerP}
5.2?for必包問題
在項目中也經常使用for循環進行啟動協程,在使用協程的時候,需要注意的for循環體中的變量也是一樣,比如:
踩坑分析:?這個問題和上面的指針問題類似,因為for遍歷非常快,所以當for遍歷完畢后,v的值是最后的值。因此,在go閉包函數運行的時候,打印的全部都是最新的值。?如何避坑:
在循環中的閉包,應該使用傳參的方式,將變量傳入函數中。這個時候會發生一次拷貝,因此,不會被其它的變量所覆蓋:
for _, v := range s {go func(v string) {println(v)}(v) } ?
或者使用臨時變量,將循環體中值重新賦值給臨時變量中:
for _, v := range s {tempV:=vgo func() {println(tempV)}() }
5.3?switch多個case問題
在項目中也會使用到switch,但是由于go語言跟其他的語言的switch,也很容易誤以為多個case放在一起能夠接著執行,如下:
package mainimport "fmt"func main() {i := 1switch i {case 1:case 2:fmt.Println("ok")}fmt.Println("end") } ?
踩坑分析:?golang的switch和其它語言差別很大。像Java/c等,上面的情況可能使case 1和case 2都執行到了下面的語句。但是golang會自動為每個case增加break。 因此,上面執行到了case 1之后就退出了。?如何避坑:如果需要上面的case滿足預期,可以在case1后面增加fallthrougth語句。 或者直接case1, 2多個條件一起。
package mainimport "fmt"func main() {i := 1switch i {case 1:fallthrougthcase 2:fmt.Println("ok")fallthrougth}fmt.Println("end") } ?
6.defer問題
6.1?defer在跨協程的問題
在項目中defer經常在使用func方法最前面,進行捕獲一些非法異常,但是也很容易忽略了跨協程的問題,比如:
//PublishBusiness 發布 func PublishBusiness(ctx context.Context, businessId int64) error {var e errordefer func() {if e !=nil{logger.CtxLogErrorf(ctx, "PublishBusiness err: %v", err) ?}}()//更新e = b.doPublishBusiness(c, businessId)go func() {1/0 //子協程 pianc}() return err } ?
踩坑分析:?defer 只會在當前函數返回前執行傳入的函數,理解這句話主要在三個方面:當前函數返回前執行傳入的函數,即 defer 關鍵值后面跟的是一個函數,包括普通函數如(fmt.Println), 也可以是匿名函數 func() 因此,在使用recover時,必須在同一個goroutine中使用才可以捕獲panic。上面出現panic是在子goroutine中,因此無法捕獲,會導致程序crash中斷退出。?如何避坑:一般啟動一個goroutine時,必須在該goroutine中處理panic,使用defer捕獲一下。
6.2?循環中使用defer
在項目中會使用到for循環打開文件,但是在關閉文件的時候容易出現問題,比如:
package mainimport ("log""os" )func main() {for i := 0; i < 10; i++ {f, err := os.Open("/path/file")if err != nil {log.Fatalln(err)}defer f.Close()} } ?
踩坑分析:?因為defer是在整個函數運行完畢之后才會執行。因此上面的代碼中,會出現內存泄漏問題,因為在循環中,每個defer函數會壓入到堆棧中。等到整個main函數執行完畢,才從堆棧中彈出來defer函數進行執行。假如循環比較大,而且里面的執行比較重,那么會嚴重影響性能。
如何避坑:不要再for循環中使用defer函數。可以通過匿名函數將函數快速結束,從而快速執行defer函數釋放資源。例如:
package main import ("log""os" ) func main() {for i := 0; i < 10; i++ {func() {f, err := os.Open("/path/file")if err != nil {log.Println(err)return}defer f.Close()}()} } ?
7.channel管道問題
7.1?channel管道panic的問題
項目經常使用協程并發,結果收集會集中在channel管道中,但在channe使用也比較容易出問題,比如:
import "time"func main() {ch := make(chan int)go func() {for i := 0; i < 1000; i++ {ch <- itime.Sleep(1)}}()go func() {close(ch)}()time.Sleep(100000) }
踩坑分析:?在channel錯誤操作比較容易影響panic,下面幾類:a).向已關閉的channel發送數據導致panicb).重復關閉channel會導致panicc).關閉nil channel會導致panic因此在上面的例子就是向已關閉的channel發送數據導致panic,會導致程序不可用?如何避坑: channel關閉要適當,也不要向關閉的channel中進行操作,包括發送信息,再次關閉等
7.2?channel管道死鎖的問題
因為在channel存在生產者和消費者,也容易出現問題,比如:
package mainfunc main() {ch := make(chan int)ch <- 1<-ch} ?
踩坑分析:?造成死鎖的原因:循環等待、資源共享、非搶占式,?在并發中出現通道死鎖有兩種情況:數據要發送,但是沒有人接收數據要接收,但是沒有人發送 因此上面就是,因為生產者和消費者在同一個goroutine中,因此無法并行執行,導致發送的消息一直無法被消費掉,而在ch<- 1一直阻塞著,出現死鎖。?如何避坑: 生產者和消費者不能屬于同一個goroutine,且生成者和消費者應該成對出現
8.sync同步機制panic問題
在并發下,sync同步機制也經常使用,也是比較容易出現問題的,比如,sync.Mutex:
package mainimport "sync"func main() {var r sync.Mutexr.Lock()r.Unlock()r.Unlock() } ?
踩坑分析:?在同步機制上造成panic會有以下情況: a).sync.Mutex 沒有加鎖就進行解鎖而導致panic b).sync.Mutex 重復解鎖而導致panic c).sync.WaitGroup 計數為負而導致panic 因此上面就是,sync.Mutex 重復解鎖而導致panic?如何避坑: 加鎖和解鎖配對出現
9?select+timer
項目中在一些情況下需要進行超時控制,使用select+timer去解決超時控制,這邊也會有一個坑,比如:
package main import ("fmt""time" )func main() {ch := make(chan string)go func() {for i := 0; i < 100; i++ {ch <- "ok"}}()for {select {case v := <-ch:fmt.Println(v)case <-time.After(time.Second * 10):fmt.Println("timeout")}} } ?
踩坑分析:?在for循環每次select的時候,都會實例化一個一個新的定時器。該定時器在10秒后 ,才會被激活,但是激活后已經跟select無引用關系,被gc給清理掉。 換句話說,被遺棄的time.After定時任務還是在時間堆里面,定時任務未到期之前,是不會被gc清理的。因此,會出現內存泄漏的現象。
如何避坑:
a).改為timer的方式:
ticker := time.NewTicker(3 * time.Second) for {<-ticker.Cfmt.Println("timeout") } ?
b).使用context.WithTimeout方式:
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)defer cancel() select {case <-ch:return truecase <-ctx.Done():return false } ?
go?編輯程序員