Interface type assertion fails due to a Golang Pointer Receiver
2 min read

Interface type assertion fails due to a Golang Pointer Receiver

Golang interface type assertion failure related to map[string]interface{} and struct type method receiver. The Golang method gets two receiver types: value and pointer whose interface type asserting are different. Let's take a deep insight into it.
Interface type assertion fails due to a Golang Pointer Receiver
Photo by cottonbro

1. problem description

I found a weird output when I tried to do some type asserting in Go with the same struct type, the sample code is as follows and you can run it in go playground:

package main

import (
	"context"
	"fmt"
	"net/http"
)

type GRPCPlugin interface {
	GRPCServer() error
	GRPCClient(context.Context) (interface{}, error)
}

type Biz interface {
	Handle(ctx context.Context, req *http.Request) error
}

type A struct {
	Plugin
	Impl Biz
}

func (p *A) GRPCServer() error {
	return nil
}

func (p *A) GRPCClient(ctx context.Context) (interface{}, error) {
	return nil, nil
}

type Test struct {
	Name string
}

// Test implement the interface sdkplugin.Biz
func (t *Test) Handle(ctx context.Context, req *http.Request) error {
	return nil
}

type Plugin interface {
	Server() (interface{}, error)
	Client() (interface{}, error)
}

type Config struct {
	Plugins map[string]Plugin
	Config  string
}

func main() {
	pluginConfig := Config{
		Plugins: map[string]Plugin{
			"cmd": &A{
				Impl: &Test{
					Name: "cmd",
				},
			},
			"xyz": A{
				Impl: &Test{
					Name: "xyz",
				},
			},
		},
		Config: "test",
	}

	// result:
	// for cmd, output is `check interface ok!`
	// for xyz, output is `check interface ok!`
	for k, p := range pluginConfig.Plugins {
		switch p.(type) {
		case GRPCPlugin:
			fmt.Println(k, " check interface ok!")
		default:
			fmt.Println(k, " check interface failed!")
		}
	}
}

The output of main.go is as follows:

cmd  check interface ok!
xyz  check interface failed!

2. analysis

1. pointer receiver V.S. value receiver

You can declare methods with pointer receivers.

This means the receiver type has the literal syntax *T for some type T. (Also, T cannot itself be a pointer such as *int.)

For example, the Scale method here is defined on *Vertex.

Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

Try removing the * from the declaration of the Scale function on line 16 and observe how the program's behavior changes.

With a value receiver, the Scale method operates on a copy of the original Vertex value. (This is the same behavior as for any other function argument.) The Scale method must have a pointer receiver to change the Vertex value declared in the main function.

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

2. map[string]interface{} initialization

Why the following initialization is OK?

pluginConfig := Config{
	Plugins: map[string]Plugin{
		"cmd": &A{
			Impl: &Test{
				Name: "cmd",
			},
		},
		"xyz": A{
			Impl: &Test{
				Name: "xyz",
			},
		},
	},
	Config: "test",
}

A is an interface type and the type map[string]A{} is similar to the type map[string]interface{} or map[string]any. For map[string]A{}, any interface type or struct type that can be converted to A are both ok. So the initialization code works.

3. how to fix

A{} is not the GRPCPlugin interface, but &A{} is the GRPCPlugin interface because the method receiver type is a pointer.

References

  1. A Tour of Go
  2. map[string]interface{} in Go

Public discussion