少年阿賓那些青春的歲月 |
2017年8月2日 #
摘要: 前陣子從支付寶轉(zhuǎn)賬1萬塊錢到余額寶,這是日常生活的一件普通小事,但作為互聯(lián)網(wǎng)研發(fā)人員的職業(yè)病,我就思考支付寶扣除1萬之后,如果系統(tǒng)掛掉怎么辦,這時余額寶賬戶并沒有增加1萬,數(shù)據(jù)就會出現(xiàn)不一致狀況了。上述場景在各個類型的系統(tǒng)中都能找到相似影子,比如在電商系統(tǒng)中,當(dāng)有用戶下單后,除了在訂單表插入一條記錄外,對應(yīng)商品表的這個商品數(shù)量必須減1吧,怎么保證?!在搜索廣告系統(tǒng)中,當(dāng)用戶點(diǎn)擊某廣告后,除了在點(diǎn)擊... 閱讀全文
微服務(wù)架構(gòu)采用Scale Cube方法設(shè)計應(yīng)用架構(gòu),將應(yīng)用服務(wù)按功能拆分成一組相互協(xié)作的服務(wù)。每個服務(wù)負(fù)責(zé)一組特定、相關(guān)的功能。每個服務(wù)可以有自己獨(dú)立的數(shù)據(jù)庫,從而保證與其他服務(wù)解耦。 微服務(wù)優(yōu)點(diǎn) 1、通過分解巨大單體式應(yīng)用為多個服務(wù)方法解決了復(fù)雜性問題,每個微服務(wù)相對較小 2、每個單體應(yīng)用不局限于固定的技術(shù)棧,開發(fā)者可以自由選擇開發(fā)技術(shù),提供API服務(wù)。 3、每個微服務(wù)獨(dú)立的開發(fā),部署 4、單一職責(zé)功能,每個服務(wù)都很簡單,只關(guān)注于一個業(yè)務(wù)功能 5、易于規(guī)模化開發(fā),多個開發(fā)團(tuán)隊(duì)可以并行開發(fā),每個團(tuán)隊(duì)負(fù)責(zé)一項(xiàng)服務(wù) 6、改善故障隔離。一個服務(wù)宕機(jī)不會影響其他的服務(wù) 微服務(wù)缺點(diǎn): 1.開發(fā)者需要應(yīng)對創(chuàng)建分布式系統(tǒng)所產(chǎn)生的額外的復(fù)雜因素 l 目前的IDE主要面對的是單體工程程序,無法顯示支持分布式應(yīng)用的開發(fā) l 測試工作更加困難 l 需要采用服務(wù)間的通訊機(jī)制 l 很難在不采用分布式事務(wù)的情況下跨服務(wù)實(shí)現(xiàn)功能 l 跨服務(wù)實(shí)現(xiàn)要求功能要求團(tuán)隊(duì)之間的緊密協(xié)作 2.部署復(fù)雜 3.內(nèi)存占用量更高
JDK 的 HashMap 中使用了一個 hash 方法來做 bit shifting,在注釋中說明是為了防止一些實(shí)現(xiàn)比較差的hashCode() 方法,請問原理是什么?JDK 的源碼參見:GrepCode: java.util.HashMap (.java) /** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } PS:網(wǎng)上看見有人說作者本人說原理需要參見圣經(jīng)《計算機(jī)程序設(shè)計藝術(shù)》的 Vol.3 里頭的介紹,不過木有看過神書,求達(dá)人介紹 這段代碼叫“擾動函數(shù)”。 題主貼的是Java 7的HashMap的源碼,Java 8中這步已經(jīng)簡化了,只做一次16位右位移異或混合,而不是四次,但原理是不變的。下面以Java 8的源碼為例解釋, //Java 8中的散列值優(yōu)化函數(shù)staticfinalinthash(Objectkey){inth;return(key==null)?0:(h=key.hashCode())^(h>>>16);//key.hashCode()為哈希算法,返回初始哈希值} 大家都知道上面代碼里的key.hashCode()函數(shù)調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int型散列值。理論上散列值是一個int型,如果直接拿散列值作為下標(biāo)訪問HashMap主數(shù)組的話,考慮到2進(jìn)制32位帶符號的int表值范圍從-2147483648到2147483648。前后加起來大概40億的映射空間。只要哈希函數(shù)映射得比較均勻松散,一般應(yīng)用是很難出現(xiàn)碰撞的。但問題是一個40億長度的數(shù)組,內(nèi)存是放不下的。你想,HashMap擴(kuò)容之前的數(shù)組初始大小才16。所以這個散列值是不能直接拿來用的。用之前還要先做對數(shù)組的長度取模運(yùn)算,得到的余數(shù)才能用來訪問數(shù)組下標(biāo)。源碼中模運(yùn)算是在這個indexFor( )函數(shù)里完成的。 bucketIndex = indexFor(hash, table.length);indexFor的代碼也很簡單,就是把散列值和數(shù)組長度做一個"與"操作, static int indexFor(int h, int length) { return h & (length-1);}順便說一下,這也正好解釋了為什么HashMap的數(shù)組長度要取2的整次冪。因?yàn)檫@樣(數(shù)組長度-1)正好相當(dāng)于一個“低位掩碼”。“與”操作的結(jié)果就是散列值的高位全部歸零,只保留低位值,用來做數(shù)組下標(biāo)訪問。以初始長度16為例,16-1=15。2進(jìn)制表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結(jié)果就是截取了最低的四位值。 10100101 11000100 00100101& 00000000 00000000 00001111---------------------------------- 00000000 00000000 00000101 //高位全部歸零,只保留末四位 但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴(yán)重。更要命的是如果散列本身做得不好,分布上成等差數(shù)列的漏洞,恰好使最后幾個低位呈現(xiàn)規(guī)律性重復(fù),就無比蛋疼。這時候“擾動函數(shù)”的價值就體現(xiàn)出來了,說到這里大家應(yīng)該猜出來了。看下面這個圖, ![]() 右位移16位,正好是32bit的一半,自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機(jī)性。而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。最后我們來看一下PeterLawley的一篇專欄文章《An introduction to optimising a hashing strategy》里的的一個實(shí)驗(yàn):他隨機(jī)選取了352個字符串,在他們散列值完全沒有沖突的前提下,對它們做低位掩碼,取數(shù)組下標(biāo)。 ![]() 結(jié)果顯示,當(dāng)HashMap數(shù)組長度為512的時候,也就是用掩碼取低9位的時候,在沒有擾動函數(shù)的情況下,發(fā)生了103次碰撞,接近30%。而在使用了擾動函數(shù)之后只有92次碰撞。碰撞減少了將近10%。看來擾動函數(shù)確實(shí)還是有功效的。但明顯Java 8覺得擾動做一次就夠了,做4次的話,多了可能邊際效用也不大,所謂為了效率考慮就改成一次了。 ------------------------------------------------------ https://www.zhihu.com/question/20733617 Go語言沒有沿襲傳統(tǒng)面向?qū)ο缶幊讨械闹T多概念,比如繼承、虛函數(shù)、構(gòu)造函數(shù)和析構(gòu)函數(shù)、隱藏的this指針等。
方法Go 語言中同時有函數(shù)和方法。方法就是一個包含了接受者(receiver)的函數(shù),receiver可以是內(nèi)置類型或者結(jié)構(gòu)體類型的一個值或者是一個指針。所有給定類型的方法屬于該類型的方法集。 如下面的這個例子,定義了一個新類型Integer,它和int一樣,只是為它內(nèi)置的int類型增加了個新方法Less() ![]() type Integer int func (a Integer) Less(b Integer) bool { return a < b } func main() { var a Integer = 1 if a.Less(2) { fmt.Println("less then 2") } } ![]() 可以看出,Go語言在自定義類型的對象中沒有C++/Java那種隱藏的this指針,而是在定義成員方法時顯式聲明了其所屬的對象。
method的語法如下: func (r ReceiverType) funcName(parameters) (results) 當(dāng)調(diào)用method時,會將receiver作為函數(shù)的第一個參數(shù): funcName(r, parameters); 所以,receiver是值類型還是指針類型要看method的作用。如果要修改對象的值,就需要傳遞對象的指針。 指針作為Receiver會對實(shí)例對象的內(nèi)容發(fā)生操作,而普通類型作為Receiver僅僅是以副本作為操作對象,并不對原實(shí)例對象發(fā)生操作。 ![]() func (a *Ingeger) Add(b Integer) { *a += b } func main() { var a Integer = 1 a.Add(3) fmt.Println("a =", a) // a = 4 } ![]() 如果Add方法不使用指針,則a返回的結(jié)果不變,這是因?yàn)镚o語言函數(shù)的參數(shù)也是基于值傳遞。 注意:當(dāng)方法的接受者是指針時,即使用值類型調(diào)用那么方法內(nèi)部也是對指針的操作。
之前說過,Go語言沒有構(gòu)造函數(shù)的概念,通常使用一個全局函數(shù)來完成。例如: ![]() func NewRect(x, y, width, height float64) *Rect { return &Rect{x, y, width, height} } func main() { rect1 := NewRect(1,2,10,20) fmt.Println(rect1.width) } ![]()
匿名組合Go語言提供了繼承,但是采用了組合的語法,我們將其稱為匿名組合,例如: ![]() type Base struct { name string } func (base *Base) Set(myname string) { base.name = myname } func (base *Base) Get() string { return base.name } type Derived struct { Base age int } func (derived *Derived) Get() (nm string, ag int) { return derived.name, derived.age } func main() { b := &Derived{} b.Set("sina") fmt.Println(b.Get()) } ![]() 例子中,在Base類型定義了get()和set()兩個方法,而Derived類型繼承了Base類,并改寫了Get()方法,在Derived對象調(diào)用Set()方法,會加載基類對應(yīng)的方法;而調(diào)用Get()方法時,加載派生類改寫的方法。
組合的類型和被組合的類型包含同名成員時, 會不會有問題呢?可以參考下面的例子: ![]() type Base struct { name string age int } func (base *Base) Set(myname string, myage int) { base.name = myname base.age = myage } type Derived struct { Base name string } func main() { b := &Derived{} b.Set("sina", 30) fmt.Println("b.name =",b.name, "\tb.Base.name =", b.Base.name) fmt.Println("b.age =",b.age, "\tb.Base.age =", b.Base.age) } ![]()
值語義和引用語義值語義和引用語義的差別在于賦值,比如 b = a b.Modify() 如果b的修改不會影響a的值,那么此類型屬于值類型;如果會影響a的值,那么此類型是引用類型。 Go語言中的大多數(shù)類型都基于值語義,包括:
C語言中的數(shù)組比較特別,通過函數(shù)傳遞一個數(shù)組的時候基于引用語義,但是在結(jié)構(gòu)體定義數(shù)組變量的時候基于值語義。而在Go語言中,數(shù)組和基本類型沒有區(qū)別,是很純粹的值類型,例如: var a = [3] int{1,2,3} var b = a b[1]++ fmt.Println(a, b) // [1 2 3] [1 3 3] 從結(jié)果看,b=a賦值語句是數(shù)組內(nèi)容的完整復(fù)制,要想表達(dá)引用,需要用指針: var a = [3] int{1,2,3} var b = &a // 引用語義 b[1]++ fmt.Println(a, b) // [1 3 3] [1 3 3]
接口Interface 是一組抽象方法(未具體實(shí)現(xiàn)的方法/僅包含方法名參數(shù)返回值的方法)的集合,如果實(shí)現(xiàn)了 interface 中的所有方法,即該類/對象就實(shí)現(xiàn)了該接口。 Interface 的聲明格式: type interfaceName interface { //方法列表 } Interface 可以被任意對象實(shí)現(xiàn),一個類型/對象也可以實(shí)現(xiàn)多個 interface; 如下面的例子: ![]() package main import "fmt" type Human struct { name string age int phone string } type Student struct { Human //匿名字段 school string loan float32 } type Employee struct { Human //匿名字段 company string money float32 } //Human實(shí)現(xiàn)SayHi方法 func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } //Human實(shí)現(xiàn)Sing方法 func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics) } //Employee重載Human的SayHi方法 func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) } // Interface Men被Human,Student和Employee實(shí)現(xiàn) // 因?yàn)檫@三個類型都實(shí)現(xiàn)了這兩個方法 type Men interface { SayHi() Sing(lyrics string) } func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} //定義Men類型的變量i var i Men //i能存儲Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain") //i也能存儲Employee i = tom fmt.Println("This is tom, an Employee:") i.SayHi() i.Sing("Born to be wild") //定義了slice Men fmt.Println("Let's use a slice of Men and see what happens") x := make([]Men, 3) //這三個都是不同類型的元素,但是他們實(shí)現(xiàn)了interface同一個接口 x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() } } ![]()
空接口空interface(interface{})不包含任何的method,正因?yàn)槿绱耍?span style="background-color: #ffff00;">所有的類型都實(shí)現(xiàn)了空interface。空interface對于描述起不到任何的作用(因?yàn)樗话魏蔚膍ethod),但是空interface在我們需要存儲任意類型的數(shù)值的時候相當(dāng)有用,因?yàn)樗梢源鎯θ我忸愋偷臄?shù)值。它有點(diǎn)類似于C語言的void*類型。 ![]() // 定義a為空接口 var a interface{} var i int = 5 s := "Hello world" // a可以存儲任意類型的數(shù)值 a = i a = s ![]()
interface的變量里面可以存儲任意類型的數(shù)值(該類型實(shí)現(xiàn)了interface),那么我們怎么反向知道這個interface變量里面實(shí)際保存了的是哪個類型的對象呢?目前常用的有兩種方法:switch測試、Comma-ok斷言。
switch測試如下: ![]() type Element interface{} type List [] Element type Person struct { name string age int } //打印 func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 //an int list[1] = "Hello" //a string list[2] = Person{"Dennis", 70} for index, element := range list{ switch value := element.(type) { case int: fmt.Printf("list[%d] is an int and its value is %d\n", index, value) case string: fmt.Printf("list[%d] is a string and its value is %s\n", index, value) case Person: fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) default: fmt.Println("list[%d] is of a different type", index) } } } ![]()
如果使用Comma-ok斷言的話: ![]() func main() { list := make(List, 3) list[0] = 1 // an int list[1] = "Hello" // a string list[2] = Person{"Dennis", 70} for index, element := range list { if value, ok := element.(int); ok { fmt.Printf("list[%d] is an int and its value is %d\n", index, value) } else if value, ok := element.(string); ok { fmt.Printf("list[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) } else { fmt.Printf("list[%d] is of a different type\n", index) } } } ![]()
嵌入接口正如struct類型可以包含一個匿名字段,interface也可以嵌套另外一個接口。 如果一個interface1作為interface2的一個嵌入字段,那么interface2隱式的包含了interface1里面的method。
反射所謂反射(reflect)就是能檢查程序在運(yùn)行時的狀態(tài)。 使用reflect一般分成三步,下面簡要的講解一下:要去反射是一個類型的值(這些值都實(shí)現(xiàn)了空interface),首先需要把它轉(zhuǎn)化成reflect對象(reflect.Type或者reflect.Value,根據(jù)不同的情況調(diào)用不同的函數(shù))。這兩種獲取方式如下: t := reflect.TypeOf(i) //得到類型的元數(shù)據(jù),通過t我們能獲取類型定義里面的所有元素 v := reflect.ValueOf(i) //得到實(shí)際的值,通過v我們獲取存儲在里面的值,還可以去改變值
轉(zhuǎn)化為reflect對象之后我們就可以進(jìn)行一些操作了,也就是將reflect對象轉(zhuǎn)化成相應(yīng)的值,例如 tag := t.Elem().Field(0).Tag //獲取定義在struct里面的標(biāo)簽 name := v.Elem().Field(0).String() //獲取存儲在第一個字段里面的值
獲取反射值能返回相應(yīng)的類型和數(shù)值 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())
最后,反射的話,那么反射的字段必須是可修改的,我們前面學(xué)習(xí)過傳值和傳引用,這個里面也是一樣的道理。反射的字段必須是可讀寫的意思是,如果下面這樣寫,那么會發(fā)生錯誤 var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
如果要修改相應(yīng)的值,必須這樣寫 var x float64 = 3.4 p := reflect.ValueOf(&x) v := p.Elem() v.SetFloat(7.1) 上面只是對反射的簡單介紹,更深入的理解還需要自己在編程中不斷的實(shí)踐。
參考文檔: http://se77en.cc/2014/05/05/methods-interfaces-and-embedded-types-in-golang/ http://se77en.cc/2014/05/04/choose-whether-to-use-a-value-or-pointer-receiver-on-methods/ http://www.cnblogs.com/chenny7/p/4497969.html 不可或缺的函數(shù),在Go中定義函數(shù)的方式如下:
通過函數(shù)定義,我們可以看到Go中函數(shù)和其他語言中的共性和特性 共性
特性Go中函數(shù)的特性是非常酷的,給我們帶來不一樣的編程體驗(yàn)。 為特定類型定義函數(shù),即為類型對象定義方法在Go中通過給函數(shù)標(biāo)明所屬類型,來給該類型定義方法,上面的 如:
上述示例為 float64 基本類型擴(kuò)充了方法IsEqual,該方法主要是解決精度問題。 其方法調(diào)用方式為: 入?yún)⒅校绻B續(xù)的參數(shù)類型一致,則可以省略連續(xù)多個參數(shù)的類型,只保留最后一個類型聲明。如 變參:入?yún)⒅С肿儏?即可接受不確定數(shù)量的同一類型的參數(shù)如 支持多返回值前面我們定義函數(shù)時返回值有兩個r,s 。這是非常有用的,我在寫C#代碼時,常常為了從已有函數(shù)中獲得更多的信息,需要修改函數(shù)簽名,使用out ,ref 等方式去獲得更多返回結(jié)果。而現(xiàn)在使用Go時則很簡單,直接在返回值后面添加返回參數(shù)即可。 如,在C#中一個字符串轉(zhuǎn)換為int類型時邏輯代碼
而在Go中,則可以這樣實(shí)現(xiàn),邏輯精簡而明確
同時在Go中很多函數(shù)充分利用了多返回值
那么如果我只需要某一個返回值,而不關(guān)心其他返回值的話,我該如何辦呢? 這時可以簡單的使用符號下劃線”_“ 來忽略不關(guān)心的返回值。如:
命名返回值前面我們說了函數(shù)可以有多個返回值,這里我還要說的是,在函數(shù)定義時可以給所有的返回值分別命名,這樣就能在函數(shù)中任意位置給不同返回值復(fù)制,而不需要在return語句中才指定返回值。同時也能增強(qiáng)可讀性,也提高godoc所生成文檔的可讀性 如果不支持命名返回值,我可能會是這樣做的
但支持給返回值命名后,實(shí)際上就是省略了變量的聲明,return時無需寫成
函數(shù)也是“值”和Go中其他東西一樣,函數(shù)也是值,這樣就可以聲明一個函數(shù)類型的變量,將函數(shù)作為參數(shù)傳遞。 聲明函數(shù)為值的變量(匿名函數(shù):可賦值個變量,也可直接執(zhí)行)
輸出結(jié)果如下,這里表明fc 的類型為:func(string)
將函數(shù)作為入?yún)ⅲɑ卣{(diào)函數(shù)),能帶來便利。如日志處理,為了統(tǒng)一處理,將信息均通過指定函數(shù)去記錄日志,且是否記錄日志還有開關(guān)
這里輸出結(jié)果如下,count 也發(fā)生了變化
函數(shù)也是“類型”你有沒有注意到上面示例中的
這里我們定義了一個類型,專門用作記錄日志的標(biāo)準(zhǔn)接口。在stringToInt函數(shù)中如果轉(zhuǎn)換失敗則調(diào)用我自己定義的接口函數(shù)進(jìn)行日志處理,至于最終執(zhí)行的哪個函數(shù),則無需關(guān)心。 defer 延遲函數(shù)defer 又是一個創(chuàng)新,它的作用是:延遲執(zhí)行,在聲明時不會立即執(zhí)行,而是在函數(shù)return后時按照后進(jìn)先出的原則依次執(zhí)行每一個defer。這樣帶來的好處是,能確保我們定義的函數(shù)能百分之百能夠被執(zhí)行到,這樣就能做很多我們想做的事,如釋放資源,清理數(shù)據(jù),記錄日志等 這里我們重點(diǎn)來說明下defer的執(zhí)行順序
這里輸出結(jié)果如下,
有如下結(jié)論:
另外,我們常使用defer去關(guān)閉IO,在正常打開文件后,就立刻聲明一個defer,這樣就不會忘記關(guān)閉文件,也能保證在出現(xiàn)異常等不可預(yù)料的情況下也能關(guān)閉文件。而不像其他語言:
后續(xù),我將討論: 作用域、傳值和傳指針 以及 保留函數(shù)init(),main() 本筆記中所寫代碼存儲位置: |