GO語言實戰之類型的本質
寫在前面
內容為《GO語言實戰》讀書筆記之一
嗯,能力有限,書里講的很多讀不大懂,也不知是翻譯的原因,嘻,讀著很拗口
比如這個類型的值做增加或者刪除的操作這句
我們平常可能會講,這個類型的值做修改的操作
整理一下,其實還是不太懂,理解不足請小伙伴幫忙指正
主要涉及知識
類型如何接收方法
內置類型在方法和函數的傳遞
引用類型在方法和函數的傳遞
寫在前面
內容為《GO語言實戰》讀書筆記之一
嗯,能力有限,書里講的很多讀不大懂,也不知是翻譯的原因,嘻,讀著很拗口
比如這個類型的值做增加或者刪除的操作這句
我們平常可能會講,這個類型的值做修改的操作
整理一下,其實還是不太懂,理解不足請小伙伴幫忙指正
主要涉及知識
類型如何接收方法
內置類型在方法和函數的傳遞
引用類型在方法和函數的傳遞
類型如何接收方法
內置類型在方法和函數的傳遞
引用類型在方法和函數的傳遞
「 傍晚時分,你坐在屋檐下,看著天慢慢地黑下去,心里寂寞而凄涼,感到自己的生命被剝奪了。當時我是個年輕人,但我害怕這樣生活下去,衰老下去。在我看來,這是比死亡更可怕的事。--------王小波」
類型的本質
在聲明一個新類型之后,聲明一個該類型的方法之前,需要先回答一個問題:這個類型的本質是什么。
如果給這個類型增加或者刪除某個值,是要創建一個新值,還是要更改當前的值?
如何接收方法
「如果是要創建一個新值,該類型的方法就使用值接收者。如果是要修改當前值,就使用指針接收者。」
當調用使用指針接收者聲明的方法時,這個方法會共享調用方法時接收者所指向的值,即對于接收者來講,始終是一個值,方法可以理解對接收者的加工,也可以說,當對接收者進行加工生產時,一般使用指針接收方法。
當調用使用指針接收者聲明的方法時,這個方法會共享調用方法時接收者所指向的值,即對于接收者來講,始終是一個值,方法可以理解對接收者的加工,也可以說,當對接收者進行加工生產時,一般使用指針接收方法。
當調用使用值接收者聲明的方法時,會使用這個值的一個副本來執行,即用于消費這個接收者,不會對原有接收有影響。
當調用使用值接收者聲明的方法時,會使用這個值的一個副本來執行,即用于消費這個接收者,不會對原有接收有影響。
需要說明的是GOlang對方法的調用者很寬松,既允許使用值,也允許使用指針來調用方法,不必嚴格符合接收者的類型。
// 值傳遞 func (u user) notify() { fmt.Printf("Sending User Email To %s<%s>\n", u.name, u.email) } // 指針傳遞 func (u *user) changeEmail(email string) { u.email = email }
這個答案也會影響程序內部傳遞這個類型的值的方式:是按值做傳遞,還是按指針做傳遞。
「保持傳遞的一致性很重要」 。這個背后的原則是,不要只關注某個方法是如何處理這個值,而是要關注這個值的本質是什么。
內置類型在方法和函數的傳遞
內置類型是由語言提供的一組類型,數值類型、字符串類型和布爾類型,這些類型本質上是原始的類型,因此,當對這些值進行增加或者刪除的時候,會創建一個新值.即通過基本類似通過值傳遞的方式。
基于這個結論,「當把內置類型 的值傳遞給方法或者函數時,應該傳遞一個對應值的副本,即使用值傳遞」
func Trim(s, cutset string) string { if s == "" || cutset == "" { return s } return TrimFunc(s, makeCutsetFunc(cutset)) }
標準庫里 strings 包的 Trim 函數,這個函數對調用者原始的string值的一個副本做操作,并返回一個新的string值的副本。字符串(string)就像整數、浮點數和布爾值一樣,本質上是一種很原始的數據值,所以在函數或方法內外傳遞時,要傳遞字符串的一份副本。
func isShellSpecialVar(c uint8) bool { switch c { case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': return true } return false }
env 包里的 isShellSpecialVar 函數。這個函數傳入了一個 int8類型的值,并返回一個bool 類型的值,這里的參數沒有使用指針來共享參數的值,調用者傳入了一個uint8值的副本,接受一個返回值 true 或者 false。(go里面是支持switch的,但是python是不支持的)
引用類型在方法和函數的傳遞
Go 語言里的引用類型有如下幾個:切片、映射、通道、接口和函數類型
當聲明上述類型的變量時,創建的變量被稱作標頭(header)值。從技術細節上說,字符串也是一種引用類型。
每個引用類型創建的標頭值是包含一個指向底層數據結構的指針。每個引用類型還包含一組獨特的字段,用于管理底層數據結構。因為標頭值是為復制而設計的,所以永遠不需要共享一個引用類型的值。類似Linux里面軟鏈接的作用。
標頭值里包含一個指針,因此通過復制來傳遞一個引用類型的值的副本,本質上就是在共享底層數據結構。
通過已有類型聲明一個用戶定義類型IP
type IP []byte //名為 IP 的類型,這個類型被聲明為字節切片 ......
當要圍繞相關的內置類型或者引用類型來聲明方法時,直接基于已有類型來聲明用戶定義的類型(結構體)會很好用。因為編譯器只允許為用戶定義的類型聲明方法.
func (ip IP) MarshalText() ([]byte, error) { if len(ip) == 0 { return []byte(""), nil } if len(ip) != IPv4len && len(ip) != IPv6len { return nil, &AddrError{Err: "invalid IP address", Addr: hexString(ip)} } return []byte(ip.String()), nil }
MarshalText 方法是用 IP 類型的值接收者聲明的。一個值接收者,即IP對象通過復制來傳遞引用類型,從而不需要通過指針來共享引用類型的值。
這種傳遞方法也可以應用到函數或者方法的參數傳遞
func ipEmptyString(ip IP) string { if len(ip) == 0 { return "" } return ip.String() }
ipEmptyString 函數。這個函數需要傳入一個 IP 類型的值。 「調用者傳入的是這個引用類型的值,而不是通過引用共享給這個函數」 ,這里和方法有著本質的區別,調用者將引用類型的值的副本傳入這個函數。
這種方法也適用于函數的返回值。最后要說的是,引用類型的值在其他方面像原始的數據類型的值一樣對待。
結構類型(用戶定義類型)
「結構類型可以用來描述一組數據值,這組值的本質即可以是原始的,也可以是非原始的」
如果決定修改某個結構類型的值時,該結構類型的值不應該被更改,需要遵守之前提到的內置類型和引用類型的規范。
簡單來講。對于一些結構體的值。可以看做是find的,不可被修改,只能創建返回一個新值, 所有使用值接收行為,我們看一個Time結構體的Demo
type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // unsigned wall seconds since Jan 1 year 1885, and ext holds a // signed 64-bit monotonic clock reading, nanoseconds since process start. wall uint64 ext int64 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location }
Time 結構選自 time 包,時間點的時間是不能修改的,看下Now 函數的實現
//func now() (sec int64, nsec int32, mono int64) func Now() Time { sec, nsec, mono := now() mono -= startNano sec += unixToInternal - minWall if uint64(sec)>>33 != 0 { return Time{uint64(nsec), sec + minWall, Local} } return Time{hasMonotonic | uint64(sec)< 這個函數創建了一個Time類型的值,并給調用者返回了Time值的副本。這個函數沒有使用指針來共享Time值。即我們對于原始類型,行為一般通過值接收,讓我們來看一個 Time 類型的方法 func (t Time) Add(d Duration) Time { dsec := int64(d / 1e9) nsec := t.nsec() + int32(d%1e9) if nsec >= 1e9 { dsec++ nsec -= 1e9 } else if nsec < 0 { dsec-- nsec += 1e9 } t.wall = t.wall&^nsecMask | uint64(nsec) // update nsec t.addSec(dsec) if t.wall&hasMonotonic != 0 { te := t.ext + int64(d) if d < 0 && te > t.ext || d > 0 && te < t.ext { // Monotonic clock reading now out of range; degrade to wall-only. t.stripMono() } else { t.ext = te } } return t } 這個方法使用值接收者,并返回了一個新的 Time 值,該方法操作的是調用者傳入的 Time 值的副本,并且給調用者返回了一個方法內的 Time 值的副本。 至于是使用返回的值替換原來的 Time 值,還是創建一個新的 Time 變量來保存結果,是由調用者決定的事情。 大多數情況下,結構類型的本質并不是原始的,而是非原始的。這種情況下,對這個類型的值做修改操作應該更改值本身。 當需要修改值本身時,在程序中其他地方,需要使用指針來共享這個值。讓我們看一個由標準庫中實現的具有非原始本質的結構類型的例子 「\Go\src\os\types.go」 //D:\Go\src\os\types.go type File struct { *file // os specific } 「\Go\src\os\file_windows.go」 //file 是*File 的實際表示 // 額外的一層結構保證沒有哪個系統的客戶端 // 能夠覆蓋這些數據。如果覆蓋這些數據, // 可能在變量終結時關閉錯誤的文件描述符 type file struct { pfd poll.FD name string dirinfo *dirInfo // 除了目錄結構,此字段為 nil appendMode bool // whether file is opened for appending } 標準庫中聲明的 File 類型。這個類型的本質是非原始的,這個類型的值實際上不能安全復制(可以理解為沒有讀鎖)。因為沒有方法阻止程序員進行復制,所以File類型的實現使用了一個嵌入的指針,指向一個未公開的類型.正是這層額外的內嵌類型阻止了復制。 我理解通過指針內嵌的方式,對File私有化,在多重讀寫中,保證了文件不被覆蓋。 不是所有的結構類型都需要或者應該實現類似的額外保護。程序員需要能識別出每個類型的本質,并使用這個本質來決定如何組織類型。 Open 函數的實現 func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) } 調用者得到的是一個指向 File 類型值的指針。Open 創建了 File 類型的值,并返回指向這個值的指針。「如果一個創建用的工廠函數返回了一個指針,就表示這個被返回的值的本質是非原始的。」 即便函數或者方法沒有直接改變非原始的值的狀態,依舊應該使用共享的方式傳遞. 即使沒有修改接收者的值,依然是用指針接收者來聲明的。因為 File 類型的值具備非原始的本質,所以總是應該被共享,而不是被復制。 「是使用值接收者還是指針接收者,不應該由該方法是否修改了接收到的值來決定。這個決策應該基于該類型的本質。」 這條規則的一個例外是,需要讓類型值符合某個接口的時候,即便類型的本質是非原始本質的,也可以選擇使用值接收者聲明方法。這樣做完全符合接口值調用方法的機制。 這部分還是有些不太明白,之后有時間還需要在看看。 Go
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。