Implementing RPC Function Calls with Reflection in Go
Reflection in Go enables dynamic function invocation, which can be leveraged to build a simple RPC system without modifying protocol definitions for each new service. This approach supports a range of parameter types including boolean, integer, floating-point, string, and byte slice.
Supported Paramter Types bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, string, byte, []byte
Target Function for Invocation
func DemoReflectFunc(paramBool bool, paramInt int, paramInt8 int8, paramInt16 int16, paramInt32 int32, paramInt64 int64, paramUint uint, paramUint8 uint8, paramUint16 uint16, paramUint32 uint32, paramUint64 uint64, paramFloat32 float32, paramFloat64 float64, paramString string, paramByte byte, paramBytes []byte) {
fmt.Println("paramBool:", paramBool)
fmt.Println("paramInt:", paramInt)
fmt.Println("paramInt8:", paramInt8)
fmt.Println("paramInt16:", paramInt16)
fmt.Println("paramInt32:", paramInt32)
fmt.Println("paramInt64:", paramInt64)
fmt.Println("paramUint:", paramUint)
fmt.Println("paramUint8:", paramUint8)
fmt.Println("paramUint16:", paramUint16)
fmt.Println("paramUint32:", paramUint32)
fmt.Println("paramUint64:", paramUint64)
fmt.Println("paramFloat32:", paramFloat32)
fmt.Println("paramFloat64:", paramFloat64)
fmt.Println("paramString:", paramString)
fmt.Println("paramByte:", paramByte)
var dataStruct *CustomData
err := json.Unmarshal(paramBytes, &dataStruct)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("dataStruct:", dataStruct)
}
Registry for Reflection Functions
var (
functionRegistry = map[string]interface{}{}
)
func initializeRegistry() {
registerFunction(DemoReflectFunc)
}
func registerFunction(funcInterface interface{}) {
funcName := runtime.FuncForPC(reflect.ValueOf(funcInterface).Pointer()).Name()
funcType := reflect.TypeOf(funcInterface)
if funcType.Kind() != reflect.Func {
return
}
functionRegistry[funcName] = funcInterface
}
Function Call Mechanism This section ancodes data in to bytes; network transmission must be implemented separately.
func InvokeFunction[T any](targetFunc T, arguments ...interface{}) {
funcName := runtime.FuncForPC(reflect.ValueOf(targetFunc).Pointer()).Name()
funcType := reflect.TypeOf(targetFunc)
if funcType.Kind() != reflect.Func {
return
}
paramCount := funcType.NumIn()
if paramCount != len(arguments) {
return
}
rpcPacket := RPCPacket{
FunctionIdentifier: funcName,
Parameters: arguments,
}
encodedData, err := serialize(rpcPacket)
if err != nil {
fmt.Println(err)
return
}
// TODO: Send encodedData to server
// TODO: Server calls ProcessRPCRequest
err = ProcessRPCRequest(encodedData)
if err != nil {
fmt.Println(err)
}
}
Function Reception and Type Conversion After decoding, types may be converted to int64 or float64, requiring manual conversion back to original types.
func ProcessRPCRequest(encodedBytes []byte) (err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Error in ProcessRPCRequest:", r)
}
}()
packet, err := deserialize(encodedBytes)
if err != nil {
return
}
funcValue := functionRegistry[packet.FunctionIdentifier]
val := reflect.ValueOf(funcValue)
if val.Kind() != reflect.Func {
return
}
funcType := reflect.TypeOf(funcValue)
if funcType.Kind() != reflect.Func {
return
}
paramCount := funcType.NumIn()
callParams := make([]reflect.Value, 0)
for i := 0; i < paramCount; i++ {
paramType := funcType.In(i)
switch paramType.Kind() {
case reflect.Int:
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int)))
case reflect.Int8:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(int8(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int8)))
}
case reflect.Int16:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(int16(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int16)))
}
case reflect.Int32:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(int32(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int32)))
}
case reflect.Int64:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(int64(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(int64)))
}
case reflect.Uint:
callParams = append(callParams, reflect.ValueOf(uint(packet.Parameters[i].(int))))
case reflect.Uint8:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(uint8(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint8)))
}
case reflect.Uint16:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(uint16(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint16)))
}
case reflect.Uint32:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(uint32(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint32)))
}
case reflect.Uint64:
if v, ok := packet.Parameters[i].(int); ok {
callParams = append(callParams, reflect.ValueOf(uint64(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(uint64)))
}
case reflect.Float32:
if v, ok := packet.Parameters[i].(float64); ok {
callParams = append(callParams, reflect.ValueOf(float32(v)))
} else {
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(float32)))
}
case reflect.Float64:
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i].(float64)))
default:
callParams = append(callParams, reflect.ValueOf(packet.Parameters[i]))
}
}
val.Call(callParams)
return
}
Serialization and Deserialization
type RPCPacket struct {
FunctionIdentifier string
Parameters []interface{}
}
func serialize(packet RPCPacket) ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
if err := encoder.Encode(packet); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func deserialize(data []byte) (RPCPacket, error) {
buffer := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buffer)
var packet RPCPacket
if err := decoder.Decode(&packet); err != nil {
return packet, err
}
return packet, nil
}
Testing the Implementation
func main() {
initializeRegistry()
testData := &CustomData{
Field1: []bool{true},
Field2: []int{2},
Field3: []int8{3},
Field4: []int16{4},
Field5: []int32{5},
Field6: []int64{6},
Field7: []uint{7},
Field8: []uint8{8},
Field9: []uint16{9},
Field10: []uint32{10},
Field11: []uint64{11},
Field12: []float32{12},
Field13: []float64{13},
Field14: []string{"14"},
Field15: []byte{15},
Field17: map[string]int{"a1": 1, "a2": 2, "a3": 3},
Field18: map[int]string{1: "a1", 2: "a2", 3: "a3"},
Field19: map[string]float64{"a1": 1.1, "a2": 2.2, "a3": 3.3},
Nested: &CustomData{
Field17: map[string]int{"a4": 4, "a5": 5, "a6": 6},
Field18: map[int]string{4: "a4", 5: "a5", 6: "a6"},
Field19: map[string]float64{"a4": 4.4, "a5": 5.5, "a6": 6.6},
},
}
jsonBytes, _ := json.Marshal(testData)
InvokeFunction(DemoReflectFunc,
true, math.MaxInt, math.MaxInt8, math.MaxInt16, math.MaxInt32,
math.MaxInt64, math.MaxInt, math.MaxUint8, math.MaxUint16, math.MaxUint32,
math.MaxInt64, math.MaxFloat32, math.MaxFloat64, "1234567890", byte(255), jsonBytes)
}
Output: paramBool: true paramInt: 9223372036854775807 paramInt8: 127 paramInt16: 32767 paramInt32: 2147483647 paramInt64: 9223372036854775807 paramUint: 9223372036854775807 paramUint8: 255 paramUint16: 65535 paramUint32: 4294967295 paramUint64: 9223372036854775807 paramFloat32: 3.4028235e+38 paramFloat64: 1.7976931348623157e+308 paramString: 1234567890 paramByte: 255 dataStruct: &{[true] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] map[a1:1 a2:2 a3:3] map[1:a1 2:a2 3:a3] map[a1:1.1 a2:2.2 a3:3.3] 0xc00013c9c0}