go advanced syntax

1. Go reflection

Reflection refers to the ability to access and modify the program itself during runtime, that is, to dynamically obtain various information about variables at runtime. Go's reflection is implemented through the type information of the interface, that is, the reflection is based on the interface type: when an entity type is assigned to an interface variable, the interface will store the entity's type information (Type) and value information (Value). So we can take advantage of reflection at runtime

1.1 Type System

The type system of most programming languages ​​is similar, with differences between declared types, actual types, and so on.

In Go reflection, an instance can be seen as two parts:

  • value
  • actual type

1.2 The two core API s of reflect ion

  • reflect.Value: used to manipulate values, some values ​​can be modified by reflection
  • reflect.Type: used to manipulate class information, class information can only be read

reflect.Type can be obtained from reflect.Value, but not the other way around.

1.3 Kind method

The reflect package makes a strong assumption: you know what Kind you are operating on.

Kind: Kind is an enumeration value used to determine the corresponding type of the operation, such as whether it is a pointer, whether it is an array, whether it is a slice, etc. So the reflect method, if you call it incorrectly, it will panic directly.

For example, in reflect.Type, these three methods have corresponding Kind What must be, otherwise panic.

Be sure to read the comments before calling the API to confirm what circumstances can be called!

code demo

Reflection outputs all field names. The key point is that only those with Kind== Struct have fields. And the pointer type (Kind== Ptr) is not!

Also consider:

  • Is the input a pointer, will it be a multiple pointer
  • Will the input be an array or slice
  • Will struct fields also be structs?

(1) Use reflection to output field names and values

// IterateFields returns all field names
// val can only be a structure, or a structure pointer, and can be multiple pointers
func IterateFields(input any) (map[string]any, error) {

   if input == nil {
      return nil, errors.New("Input object cannot be empty")
   }

   typ := reflect.TypeOf(input)
   val := reflect.ValueOf(input)

   // Handling pointers, to get what the pointer points to
   // Here we comprehensively consider the effect of multiple pointers
   for typ.Kind() == reflect.Ptr {
      typ = typ.Elem()
      val = val.Elem()
   }

   // If it is not a struct, return error
   if typ.Kind() != reflect.Struct {
      return nil, errors.New("illegal type")
   }

   num := typ.NumField()
   res := make(map[string]any, num)
   for i := 0; i < num; i++ {
      field := typ.Field(i)  //  Field information
      fieldVal := val.Field(i)  // field value information
      if field.IsExported() {
         res[field.Name] = fieldVal.Interface()
      } else {
         // In order to demonstrate the effect, we will fill the undisclosed fields with zero values
         res[field.Name] = reflect.Zero(field.Type).Interface()
      }
   }
   return res, nil
}


func TestIterateFields(t *testing.T) {
   up := &types.User{Name: "queen"}
   up2 := &up
   testCases := []struct {
      name       string
      input      any
      wantFields map[string]any
      wantErr    error
   }{
      {
         // null
         name:    "empty",
         input:   nil,
         wantErr: errors.New("Input object cannot be empty"),
      },
      {
         // common structure
         name: "normal struct",
         input: types.User{
            Name: "Tom",
         },
         wantFields: map[string]any{
            "Name": "Tom",
            "age":  0,
         },
      },
      {
         // pointer
         name: "pointer",
         input: &types.User{
            Name: "jack",
         },
         wantFields: map[string]any{
            "Name": "jack",
            "age":  0,
         },
      },
      {
         // multiple pointers
         name:  "multiple pointer",
         input: up2,
         wantFields: map[string]any{
            "Name": "queen",
            "age":  0,
         },
      },
      {
         // illegal input
         name:    "slice",
         input:   []string{},
         wantErr: errors.New("illegal type"),
      },
      {
         // illegal pointer input
         name:    "pointer to map",
         input:   &(map[string]any{}),
         wantErr: errors.New("illegal type"),
      },
   }
   for _, tc := range testCases {
      t.Run(tc.name, func(t *testing.T) {
         res, err := IterateFields(tc.input)
         assert.Equal(t, tc.wantErr, err)
         if err != nil {
            return
         }
         assert.Equal(t, tc.wantFields, res)
      })
   }
}

copy code

Be aware of field scoping issues. Reflection can get the type information of the private field, but not the value.

1.4 Pointers and structures pointed to by pointers

pointer

A pointer to the memory address of the target. Many of the effects implemented in dynamically typed languages ​​like Python, such as methods, require reference pointers in Go.

The & operator can generate a pointer. E.g--

i := 42
p := &i
 copy code

Here p is a pointer to i.

It should be noted that the * operator has a different effect than the type and pointer. *type means that what is accepted or returned is a pointer, which is often used when declaring functions. *pointer represents taking its corresponding value through the pointer pointer.

i := 42
p := &i
*p = 420
// i = 420
 copy code

In the above code, p is the pointer of i, and the *p operation is equivalent to directly reassigning i, so i will become 420. And the following function accepts a pointer p, and then also operates the target through *p.

func operPointer(p *int) {
    *p = *p * 100
    // The accepted parameter is a pointer
    // Set the value pointed to by the pointer through *pointer in the function
    // *Type represents accepting a pointer;
    // And *Pointer represents the value pointed to by the operation pointer
}
copy code

Structures & Pointers

Generally speaking, the way to access an object through a pointer is *pointer. Suppose there is a pointer to a structure. According to this logic, the way to access the structure field should be (*pointer). The pointer is optimized, and we can directly access the field of the structure pointed to by the pointer through pointer.field. So you can have a function like the following.

func Scale5P(p *MyNumbers) {
    p.X = p.X * 5
    p.Y = p.Y * 5
    //Values ​​can be accessed directly through pointers
    //The Go compiler translates to (*i).X and (*i).Y
}
copy code

The function Scale5P() accepts a pointer of type MyNumbers, but within the function, we can directly access the fields of the structure through the pointer p.

func PointerKey3() {
    i := MyNumbers{1, 2}
    // Scale5P(i)
    Scale5P(&i)
    // For the function Scale5P(), since the accepted parameter is a pointer, &i must be passed in
    fmt.Println(i)
}
copy code

code demo

(1) Set with reflection

Reflection can be used to modify the value of a field. But be sure to check CanSet before modifying the value of the field (whether the field can be modified)

In short, the structure pointer must be used, then the fields of the structure can be modified. Of course, the object pointed to by the pointer can also be modified.

func SetField(entity any, field string, newVal any) error {

   if entity == nil {
      return errors.New("Entity cannot be nil")
   }
   if field == "" {
      return errors.New("Field cannot be an empty string")
   }

   val := reflect.ValueOf(entity)

   typ := val.Type()

   if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
      return errors.New("illegal type")
   }

   typ = typ.Elem()
   val = val.Elem()
   fd := val.FieldByName(field)
   if _, found := typ.FieldByName(field); !found {
      return errors.New("field does not exist")
   }
   if !fd.CanSet() {  // Determine if the dictionary is modifiable
      return errors.New("non-modifiable fields")
   }
   // set value for field
   fd.Set(reflect.ValueOf(newVal))
   return nil
}

func TestSetField(t *testing.T) {
   testCases := []struct {
      name    string
      field   string
      entity  any
      newVal  any
      wantErr error
   }{
      {
         name:    "empty",
         field:   "Name",
         entity:  nil,
         wantErr: errors.New("Entity cannot be nil"),
      },
      {
         name:    "struct",
         field:   "Name",
         entity:  types.User{},
         wantErr: errors.New("illegal type"),
      },
      {
         name:    "private field",
         field:   "age",
         entity:  &types.User{},
         wantErr: errors.New("non-modifiable fields"),
      },
      {
         name:    "invalid field",
         entity:  &types.User{},
         field:   "invalid_field",
         wantErr: errors.New("field does not exist"),
      },
      {
         name:   "pass",
         field:  "Name",
         entity: &types.User{},
         newVal: "Tom",
      },
   }

   for _, tc := range testCases {
      t.Run(tc.name, func(t *testing.T) {
         err := SetField(tc.entity, tc.field, tc.newVal)
         assert.Equal(t, tc.wantErr, err)
      })
   }
}

copy code

(2) Output method information and execute the call

output:

  • method name
  • method parameters
  • return value

Note: Even if a field is defined as a function type, it still counts as a field, not a method. So strictly speaking, we should call the information of the fields of the output function type here, and execute.

// IterateFuncs outputs method information and executes the call
func IterateFuncs(val any) (map[string]*FuncInfo, error) {
   typ := reflect.TypeOf(val)
   if typ.Kind() != reflect.Struct || typ.Kind() != reflect.Ptr {
      return nil, errors.New("illegal type")
   }
   num := typ.NumMethod()
   result := make(map[string]*FuncInfo, num)
   for i := 0; i < num; i++ {
      f := typ.Method(i)      // get method by subscript
      numIn := f.Type.NumIn() // Get the number of parameters of the method
      ps := make([]reflect.Value, 0, numIn)
      // The first parameter is always the receiver, similar to java's this, python's self concept
      ps = append(ps, reflect.ValueOf(val))
      in := make([]reflect.Type, 0, numIn)
      for j := 0; j < numIn; j++ {
         p := f.Type.In(j) // Get the type of the input parameter of the method
         in = append(in, p)
         if j > 0 {
            // Add the value of 0 for the parameter of type p to ps
            ps = append(ps, reflect.Zero(p))
         }
      }
      // call result
      ret := f.Func.Call(ps)
      outNum := f.Type.NumOut() // Get the number of results output by the method
      out := make([]reflect.Type, 0, outNum)
      res := make([]any, 0, outNum)
      for k := 0; k < outNum; k++ {
         out = append(out, f.Type.Out(k))      // f.Type.Out(k) Get the output parameter type of the method Add the output parameter type to out
         res = append(res, ret[k].Interface()) // ret[k].Interface() Get the output result Add the result of the call to res
      }
      result[f.Name] = &FuncInfo{
         Name:   f.Name,
         In:     in,
         Out:    out,
         Result: res,
      }
   }
   return result, nil
}

type FuncInfo struct {
   Name string
   In   []reflect.Type
   Out  []reflect.Type

   // The result of the reflection call
   Result []any
}


func TestIterateFuncs(t *testing.T) {
   testCases := []struct {
      name    string
      input   any
      wantRes map[string]*FuncInfo
      wantErr error
   }{
      {
         // common structure
         name:  "normal struct",
         input: types.User{},
         wantRes: map[string]*FuncInfo{
            "GetAge": {
               Name:   "GetAge",
               In:     []reflect.Type{reflect.TypeOf(types.User{})},
               Out:    []reflect.Type{reflect.TypeOf(0)},
               Result: []any{0},
            },
         },
      },
      {
         // pointer
         name:  "pointer",
         input: &types.User{},
         wantRes: map[string]*FuncInfo{
            "GetAge": {
               Name:   "GetAge",
               In:     []reflect.Type{reflect.TypeOf(&types.User{})},
               Out:    []reflect.Type{reflect.TypeOf(0)},
               Result: []any{0},
            },
            "ChangeName": {
               Name:   "ChangeName",
               In:     []reflect.Type{reflect.TypeOf(&types.User{}), reflect.TypeOf("")},
               Out:    []reflect.Type{},
               Result: []any{},
            },
         },
      },
   }
   for _, tc := range testCases {
      t.Run(tc.name, func(t *testing.T) {
         res, err := IterateFuncs(tc.input)
         assert.Equal(t, tc.wantErr, err)
         if err != nil {
            return
         }
         assert.Equal(t, tc.wantRes, res)
      })
   }
}
copy code

Precautions:

  • method receiver
    • With a structure as input, only the method of the structure as a receiver can be accessed
    • takes a pointer as input, you can access any receiver method
  • The first parameter of the input, always the receiver itself

(3) Why can't the method implementation be modified?

Because the go runtime does not expose the interface

(4) Reflection Traversal

Traverse:

  • array
  • slice
  • string
  • map (the traversal of Map is different from the other three)
// Iterate iterates over an array, slice, or string
func IterateArray(input any) ([]any, error) {
   val := reflect.ValueOf(input)
   typ := val.Type()
   kind := typ.Kind()
   if kind != reflect.Array && kind != reflect.Slice && kind != reflect.String {
      return nil, errors.New("illegal type")
   }
   res := make([]any, 0, val.Len())
   for i := 0; i < val.Len(); i++ {
      ele := val.Index(i)
      res = append(res, ele.Interface())
   }
   return res, nil
}

// IterateMapV1 returns key, value
func IterateMapV1(input any) ([]any, []any, error) {
   val := reflect.ValueOf(input)
   if val.Kind() != reflect.Map {
      return nil, nil, errors.New("illegal type")
   }
   l := val.Len()
   keys := make([]any, 0, l)
   values := make([]any, 0, l)
   for _, k := range val.MapKeys() {
      keys = append(keys, k.Interface())
      v := val.MapIndex(k)
      values = append(values, v.Interface())
   }
   return keys, values, nil
}

// IterateMapV2 returns key, value
func IterateMapV2(input any) ([]any, []any, error) {
   val := reflect.ValueOf(input)
   if val.Kind() != reflect.Map {
      return nil, nil, errors.New("illegal type")
   }
   l := val.Len()
   keys := make([]any, 0, l)
   values := make([]any, 0, l)
   itr := val.MapRange()
   for itr.Next() {
      keys = append(keys, itr.Key().Interface())
      values = append(values, itr.Key().Interface())
   }
   return keys, values, nil
}
copy code

These values ​​can be modified by simply calling the Set method on these values, and be careful to check CanSet.

1.5 Open Source Examples

(1) Dubbo-go reflection generation proxy

In Go, the mechanism for generating proxies is a little weird. Because Go doesn't provide an API for tampering with method implementations, we're actually declaring a field of method type.

This generation proxy mechanism is the core part of designing the RPC framework.

makeDubboCallProxy is our new value to replace the old one.

(2) Beego reflection analysis model data

In the ORM framework, a very important environment is to parse the model definition. For example, when a user defines a User, the table name, column name, primary key, foreign key, index, association relationship, etc. must be parsed from it.

This part of Beego's work is done in the register method of modelCache under the orm package.

The step of parsing model metadata is important, but doesn't need to be tangled in details. Because different people design ORM frameworks, the model definition specifications are different. But the metadata required by these ORM frameworks is always similar, and that's enough to sum it up.

(3) GORM reflection analysis model data

GORM is similar, it refers to model metadata as Schema. The core code is in ParseWithSpecialTableName. , below is the entry, and a bunch of validations reveal what kind of model definitions GORM supports.

Parsing field by field, it can be clearly seen that it only parses public fields.

GORM uses tag s to allow users to set some descriptions of fields, such as whether it is a primary key or whether to allow auto-increment.

The model defined on the official website uses tag (tag).

ParseField is based on the design of GORM.

(4) Comparison of Beego and GORM model metadata

Table name, column name, primary key, foreign key, index, association. The core is to learn how these two frameworks express this information, that is, the definitions of the two structures modelInfo and Schema.

1.6 Go reflection programming tips

  • To read and write values, use reflect.Value
  • To read type information, use reflect.Type
  • Always pay attention to whether the type you are operating on is a pointer. The pointer and the object pointed to by the pointer are two things at the reflection level
  • In most cases, reflect.Type for pointer types is useless. We operate on the type pointed to by the pointer
  • Don't use reflection without enough tests, because the reflection API is full of panic s
  • Slices and arrays are also two things in reflection
  • Fields and methods of method types are also two different things in reflection

1.7 Go reflection interview points

  • There are very few Go reflection surfaces, because Go reflection itself is used for writing code, and there are not many theoretical things.
  • What is reflection? Reflection can be seen as a description of objects and types, and we can indirectly manipulate objects through reflection.
  • Reflection usage scenarios? A whole bunch, basically any high-level framework will use reflection, ORM is a typical example. Beego's controller-pattern Web framework also utilizes reflection.
  • Is it possible to modify the method through reflection? cannot. Why not? The go runtime does not expose interfaces.
  • What kind of fields can be modified by reflection? There is a method CanSet that can be judged, which is simply addressable.

2. unsafe

What is unsafe?

Go's pointers are type-safe, but they have many limitations. Go also has unsafe pointers, which is unsafe.Pointer provided by the unsafe package. In some cases it makes the code more efficient and, of course, more dangerous. The unsafe package is used by the Go compiler and is used during the compilation phase. As you can see from the name, it is not safe and is not officially recommended for use. But how can high-level Gopher not use the unsafe package? It can bypass the type system of the Go language and directly manipulate memory. For example, we generally cannot manipulate unexported members of a struct, but we can do so with the unsafe package. The unsafe package allows me to read and write memory directly, regardless of what you export or not.

Why is there unsafe?

The Go language type system is designed for safety and efficiency, and sometimes safety can lead to inefficiency. With the unsafe package, advanced programmers can use it to bypass the inefficiencies of the type system. Therefore, it has a meaning to exist, and reading the Go source code, you will find a lot of examples of using the unsafe package.

2.1 Object memory layout

To understand unsafe, the core is to understand how an object in Go is laid out in memory.

To understand object memory layout requires the following knowledge:

  • Calculate the address
  • Calculate the offset
  • in direct operation

You can run the following code example to see how the various offsets are calculated in different ways.

package unsafe

import (
   "fmt"
   "reflect"
)

// PrintFieldOffset is used to print the field offset
// Used to study memory layout
// Only accepts structs as input
func PrintFieldOffset(entity any) {
   typ := reflect.TypeOf(entity)
   for i := 0; i < typ.NumField(); i++ {
      fd := typ.Field(i)
      fmt.Printf("%s: %d \n", fd.Name, fd.Offset)
   }
}

copy code
package layout

import (
   "fmt"
   "testing"
   "unsafe"
   "unsafe/types"
)

func TestPrintFieldOffset(t *testing.T) {
   fmt.Println(unsafe.Sizeof(types.User{}))
   PrintFieldOffset(types.User{})

   fmt.Println(unsafe.Sizeof(types.UserV1{}))
   PrintFieldOffset(types.UserV1{})

   fmt.Println(unsafe.Sizeof(types.UserV2{}))
   PrintFieldOffset(types.UserV2{})
}
copy code

(1) Example of object memory layout

package types

type User struct {
   Name    string
   age     int32
   Alias   []byte
   Address string
}

copy code
=== RUN   TestPrintFieldOffset
64
Name: 0 
age: 16 
Alias: 24 
Address: 48 
--- PASS: TestPrintFieldOffset (0.00s)
PASS
 copy code

64 is the total size of the structure. The question is, why is Alias' offset 24, when it should be 20? (Because the int32 type field offset is 4) This involves the alignment rules of go.

  • Go alignment rules
    • Aligned by word length. Because every time Go itself accesses memory, it is accessed according to a multiple of the word length.

  • On a 32-bit word length machine, it is aligned by 4 bytes
  • On a 64-bit word length machine, it is aligned by 8 bytes

Looking back at the example of the object memory layout just now, add an agev1 field after the age field. After running, you can see that the offset of agev1 is 20, and the offset of Alias ​​has not changed. Why is this?

type UserV1 struct {
   Name    string
   age     int32
   agev1   int32
   Alias   []byte
   Address string
}
copy code
=== RUN   TestPrintFieldOffset
64
Name: 0 
age: 16 
agev1: 20 
Alias: 24 
Address: 48 
--- PASS: TestPrintFieldOffset (0.00s)
PASS
 copy code

64 is the total size of the structure. Because Go is aligned according to word length, on a 64-bit machine, age + agev1 is exactly one word length, so the offset of Alias ​​does not change.

2.2 Use unsafe to read and write fields

Read: *(*T)(ptr), T is the target type, if you don't know the type, you can only get the reflected Type typ, then you can use reflect.NewAt(typ, ptr).Elem().

Write: *(*T)(ptr) = T, where T is the target type.

ptr is the field offset: ptr = structure start address + field offset

type UnsafeAccessor struct {
   fields     map[string]FieldMeta //  Mainly the field offset
   entityAddr unsafe.Pointer       //  the starting address of the structure
}

func NewUnsafeAccessor(entity any) (*UnsafeAccessor, error) {
   if entity == nil {
      return nil, errors.New("invalid entity")
   }
   val := reflect.ValueOf(entity)
   typ := reflect.TypeOf(entity)
   //  typ.Elem() to get the concrete type
   elemType := typ.Elem()

   if typ.Kind() != reflect.Pointer || elemType.Kind() != reflect.Struct {
      return nil, errors.New("invalid entity")
   }
   //  typ.Elem().NumField() Get the total number of fields from the specific type of typ
   fieldNum := elemType.NumField()
   fields := make(map[string]FieldMeta, fieldNum)
   for i := 0; i < fieldNum; i++ {
      fd := elemType.Field(i)
      // fd.Offset field offset
      fields[fd.Name] = FieldMeta{typ: fd.Type, offset: fd.Offset}
   }

   // val.UnsafePointer() gets the starting address from the value
   return &UnsafeAccessor{entityAddr: val.UnsafePointer(), fields: fields}, nil
}
copy code

Note: unsafe operates on memory, which is essentially the starting address of the object.

Example of res := *(*T)(ptr) read:

func (u *UnsafeAccessor) Field(field string) (int, error) {
   fdMeta, ok := u.fields[field]
   if !ok {
      return 0, errors.New("field does not exist")
   }
   // address calculated
   ptr := unsafe.Pointer(uintptr(u.entityAddr) + fdMeta.offset)
   if ptr == nil {
      return 0, fmt.Errorf("invalid address of the field: %s", field)
   }
   // Key operation Get field address (field address) = structure start address + field offset
   // res := *(*int)(unsafe.Pointer(uintptr(u.entityAddr) + fdMeta.offset))
   res := *(*int)(ptr)
   return res, nil
}

func (u *UnsafeAccessor) FieldAny(field string) (any, error) {
   meta, ok := u.fields[field]
   if !ok {
      return 0, errors.New("field does not exist")
   }
   // address calculated
   res := reflect.NewAt(meta.typ, unsafe.Pointer(uintptr(u.entityAddr)+meta.offset))
   return res.Interface(), nil
}
copy code

Example written by *(*T)(ptr)=newVal:

func (u *UnsafeAccessor) SetField(field string, val int) error {
   fdMeta, ok := u.fields[field]
   if !ok {
      return errors.New("field does not exist")
   }
   // Calculated address Get field address (field address) = structure start address + field offset
   ptr := unsafe.Pointer(uintptr(u.entityAddr) + fdMeta.offset)
   // key operations 
   // *(*int)(unsafe.Pointer(uintptr(u.entityAddr) + fdMeta.offset)) = val
   *(*int)(ptr) = val
   return nil
}

func (u *UnsafeAccessor) SetFieldAny(field string, val any) error {
   meta, ok := u.fields[field]
   if !ok {
      return errors.New("field does not exist")
   }
   // address calculated
   res := reflect.NewAt(meta.typ, unsafe.Pointer(uintptr(u.entityAddr)+meta.offset))
   if res.CanSet() {
      res.Set(reflect.ValueOf(val))
   }
   return nil
}
copy code

2.3 unsafe.Pointer and uintptr

Earlier we used unsafe.Pointer and uintptr, both of which represent pointers, so what's the difference?

  • unsafe.Pointer: a pointer at the Go level, the GC will maintain the value of unsafe.Pointer
  • uintptr: directly is a number, representing a memory address (the storage address of the pointer)
  • unsafe.Pointer(s) converts the s pointer type to the unsafe.Pointer non-type safe pointer type, and uintptr gets the value (this value stores the address value of the s pointer, that is, the initial value pointed to by s).

type FieldMeta struct {
   typ reflect.Type
   // When we consider combination or complex type field later, its meaning is derived to express the offset equivalent to the outermost structure
   offset uintptr
}

type UnsafeAccessor struct {
   fields     map[string]FieldMeta //  Mainly the field offset
   entityAddr unsafe.Pointer       //  the starting address of the structure
}
copy code

2.4 unsafe.Pointer and GC

Suppose that the previous unsafe.Pointer represents the pointer to the object before the GC, and the address it points to is 0xAAAA.

If a GC occurs, the object is still alive after the GC, but at this time the object is copied to another location (the Go GC algorithm is mark-copy). Then the unsafe.Pointer representing the object will be corrected by GC and point to the new address 0xAABB.

2.5 Misunderstandings in the use of uintptr

If you use uintptr to save the starting address of the object, then if a GC occurs, the original code will crash directly.

type UnsafeAccessor struct {
   fields     map[string]FieldMeta //  Mainly the field offset
   entityAddr unsafe.Pointer       //  the starting address of the structure
}


type UnsafeAccessorV1 struct {
   fields     map[string]FieldMeta
   entityAddr uintptr
}
copy code

For example, before the GC, the calculated entityAddr = 0xAAAA, then after the GC, the actual address becomes 0xAABB due to replication. Because the GC does not maintain the uintptr type variable (only maintains the unsafe.Pointer type), the entityAddr is still 0xAAAA. At this time, if you use 0xAAAA as the starting address to access the field, you will not know what to access.

But uintptr can be used to express relative quantities, such as field offsets. The offset of this field will not change no matter what the GC is. If you are afraid of making mistakes, use uintptr only when performing address operations, and use unsafe.Pointer at other times.

type FieldMeta struct {
   typ reflect.Type
   // When we consider combination or complex type field later, its meaning is derived to express the offset equivalent to the outermost structure
   offset uintptr
}
copy code

2.6 unsafe Interview Points

  • The difference between uintptr and unsafe.Pointer: the former represents a specific address, and the latter represents a logical pointer. In the latter case, in the case of GC, go runtime will help you adjust it so that it always points to the address where the real object is stored.
  • How are Go objects aligned? According to word length. Some nasty interviewers may ask you to manually demonstrate how to align, or write an object and ask you how to calculate the size of the object.
  • How to calculate the object address? The starting address of the object is obtained by reflection, and the address of the internal field of the object is calculated by starting address + field offset.
  • Why is unsafe more efficient than reflection? It can be simply considered that reflection helps us encapsulate many unsafe operations, so we directly use unsafe to bypass the overhead of this encapsulation. It's a bit like we don't use the ORM framework, but directly write SQL to execute the query.

Tags: Java C++ Go

Posted by hyster on Sat, 22 Oct 2022 01:32:06 +0530