前言
首先要明白,Go中結構體方法的定義方式有兩種,包括指針方法和值方法。
- 如果一個方法的接收者的類型是其所屬類型的指針類型(並非該類型本身),則該方法稱爲一個指針方法。
- 如果一個方法的接收者類型就是其所屬的類型本身,則該方法稱爲做值方法。
示例:
//指針方法
func (e *Employee) UpdateAge(newVal int){
e.Age=newVal
}
//值方法
func (e Employee) UpdateAge(newVal int){
e.Age=newVal
}
那麼指針方法和值方法的區別在哪呢?
- (e Employee)的定義是實例成員的拷貝,而不是成員本身;在實例對應方法被調用時,實例的成員會進行復制
- (s *Student)的定義是指針的拷貝,只創建了指針的副本,若操作指針的話,操作的也是實例本身;同時該方法避免了大量數據的內存拷貝(一般情況下,推薦使用這種方式)。
示例:
func (e *Employee) UpdateAge(newVal int){
e.Age=newVal
}
func TestMethod(t *testing.T){
e:=Employee{"1","Amy",20}
e.UpdateAge(10)
fmt.Println(e.GetAge())
}
//output:
10
func (e Employee) UpdateAge(newVal int){
e.Age=newVal
}
func TestMethod(t *testing.T){
e:=Employee{"1","Amy",20}
e.UpdateAge(10)
fmt.Println(e.GetAge())
}
//output:
20
顯而易見,e *Employee的方式操作了指針,指針指向了統一的對象,操作也即同一個對象;而e Employee的方式copy了一次對象,只對局部的對象進行了修改。
-
爲什麼e *Employee可以直接.Age,而不是類似與C++的e->Age或者( * e).Age呢?
其實Go在發送e爲一個指針類型,同時其在訪問某個字段時,就會將該表達式視爲(*e).Age。
反射
FieldByName
在反射中,如果不稍加註意,*的加與不加就經常會導致panic,同樣用以上的Employee舉個栗子:
e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).FieldByName("Name")
運行後報錯“call of reflect.Value.FieldByName on ptr Value”:FieldByName不可操作指針類型
進入源碼:
// FieldByName returns the struct field with the given name.
// It returns the zero Value if no field was found.
// It panics if v's Kind is not struct.
func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}
func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type " + t.String())
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}
在FieldByName中,對v的類型進行的判斷,規定傳入的爲Struct的類型,否則將報錯。
fmt.Println(reflect.ValueOf(e).Kind())
//output:
ptr
當將示例改成如下時:
e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).FieldByName("Name")
將成功運行獲取Name的值,參考以上,打印reflect.ValueOf(*e)的Kind類型
fmt.Println(reflect.ValueOf(*e).Kind())
//output:
struct
MethodByName
UpdateAge如下:
func (e *Employee) UpdateAge(newVal int){
e.Age=newVal
}
e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).MethodByName("UpdateAge").
Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)
//output:
New Age: 1
以上代碼是合理的結果,通過反射設置e的Age;
設置修改下代碼,如下:
e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).MethodByName("UpdateAge").
Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)
將e改成*e;
運行後報錯“reflect: call of reflect.Value.Call on zero Value [recovered]”,進入源碼探索下:
MethodByName:
func (v Value) MethodByName(name string) Value {
if v.typ == nil {
panic(&ValueError{"reflect.Value.MethodByName", Invalid})
}
if v.flag&flagMethod != 0 {
return Value{}
}
m, ok := v.typ.MethodByName(name)
if !ok {
return Value{}
}
return v.Method(m.Index)
}
v.typ.MethodByName:
func (t *rtype) MethodByName(name string) (m Method, ok bool) {
if t.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(t))
return tt.MethodByName(name)
}
ut := t.uncommon()
if ut == nil {
return Method{}, false
}
// TODO(mdempsky): Binary search.
for i, p := range ut.exportedMethods() {
if t.nameOff(p.name).name() == name {
return t.Method(i), true
}
}
return Method{}, false
}
先查看下uncommon方法:
func (t *rtype) uncommon() *uncommonType {
if t.tflag&tflagUncommon == 0 {
return nil
}
switch t.Kind() {
case Struct:
return &(*structTypeUncommon)(unsafe.Pointer(t)).u
case Ptr:
type u struct {
ptrType
u uncommonType
}
return &(*u)(unsafe.Pointer(t)).u
....
}
其對t的類型進行了判斷並做相應的返回處理;
exportedMethods:
func (t *uncommonType) exportedMethods() []method {
if t.xcount == 0 {
return nil
}
return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount]
}
發現mcount和xcount均爲0;
mcount uint16 // number of methods
xcount uint16 // number of exported methods
我們試着將*e改爲e,即:
e:=&Employee{"1","Amy",20}
reflect.ValueOf(e).MethodByName("UpdateAge").
Call([]reflect.Value{reflect.ValueOf(1)})
在此種方式在,發現mcount和xounct均爲1,即方法數量均爲1。
這不禁讓我們很疑惑,爲何如此?
首先,在上面也講到,在傳入*e時,其實我們傳入的是struct,在傳入e時,傳入的爲ptr指針類型,而對於Go來說,有這樣一條規則:一個指針類型擁有以它以及以它的基底類型爲接收者類型的所有方法,而它的基底類型卻只擁有以它本身爲接收者類型的方法。
指針類型與基底類型其實是相對的概念:指針類型由指針加上某個基底類型組成,基底類型如上面所說的Employee,其指針類型就是*Employee。
回到上面的規則,也就是說*Employee擁有UpdateAge,而Employee沒有此方法。
-
也就是說,如果我們新增一個GetAge方法
func (e Employee) GetAge() int{ return e.Age }
這個時候*Employee擁有UpdateAge和GetAge,而Employee只擁有GetAge。
-
爲什麼我們定義一個Employee的對象e1,可以直接e1.UpdateAge呢?
如果Go語言發現調用的UpdateAge方法是e1的指針方法,則其會將表達式視爲(&e1).GrUpdateAgeow()。
我們可以進行測試,將UpdateAge方法修改成如下:
func (e Employee) UpdateAge(newVal int){
e.Age=newVal
}
此時無論是*Employee還是Employee,都擁有了此方法,再次運行代碼:
e:=&Employee{"1","Amy",20}
reflect.ValueOf(*e).MethodByName("UpdateAge").
Call([]reflect.Value{reflect.ValueOf(1)})
fmt.Println("New Age:",e.Age)
//output:
New Age: 20
發現Age爲20,並未改變,這在前言已經說過,值傳遞中的修改只是修改了拷貝的副本,並未修改對象本身。
至此,大功告成,茅塞頓開。
總結
由於校招拿到offer的企業後臺技術棧主要爲Go,最近經歷從Java轉Golang的一個過程,以下是學習了幾天後的小感悟吧:首先在Java中由於少了指針這一Part,在Java中通過引用地址的值傳遞會比較直接,無需顧慮太多,而對於Golang而言,其確實方便,關鍵字又少,雖說存在指針類型,但實際上Go開發團隊暗中幫助我們屏蔽了很多底層的細節,如以上提到的(*e).Age、(&e1).GrUpdateAgeow()等表達式的速記法,會讓開發者避免了很多bug,這其實也給我們在探索底層實現時稍稍加了點挑戰吧,也算有利有弊。