Engineering Blog

                            

Any says nothing

The interface type that specifies zero methods is known as the empty interface: interface{} An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type. With Go 1.18 the predeclared type any became an alias for an empty interface. Any can replace all the interface{} occurrences. With any type definition, Go has replaced all references to interface{}. Any type can hold any value type.

func main() {
var i any
i = 42
i = "foo"
i = struct {
s string
}{
s: "bar",
}
i = f
}

While assigning the value to any type users lose all type information, which requires type assertion to get any use of the above-declaired variable.

package store
type Product struct{
id int
product string
}
type Price struct{
id int
price float64
}
type Store struct{}
func (s *Store) Get(id string) (any, error) {

} 
func (s *Store) Set(id string, v any) error {

}

Although there are no issues with store implementation and compilation-wise one should think about method signatures. Here, we accept and return any arguments, the method lack expressiveness. If store struct needs to be used in the future it will be difficult to understand the implementation and method and have to dig into the reference or documentation. Hence, accepting or returning any type doesn’t convey meaningful information. Also, because there is no safeguard at compile time, nothing prevents a caller from calling these methods with whatever data type, such as an int :

s := store.Store{}
s.Set("code", 42)

Using any we lose some of the benefits of go such as statically typed language. Instead we should avoid the use of any type and make our signatures explicit as much as possible. Regarding our example, this could mean duplicating the Get and Set methods per type:

func (s *Store) GetProduct(id string) (Product, error) {

}
func (s *Store) SetProduct(id string, product Product) error {

}
func (s *Store) GetPrice(id string) (Price, error) {

}
func (s *Store) SetPrice(id string, price Price) error {

}

In the above implementation, the methods are expressive and easy to understand. Though more methods are used it is not the defect as we can use the interface to make abstraction to the method and use only the method necessary for the implementation. For example, if a client is interested only in the product methods, it could write something like this:

type ProductStorer interface {
GetProduct(id string) (store.Product, error)
SetProduct(id string, product store.Product) error
}

What are the cases when any is helpful?

In the encoding/JSON package, any is well-fitted because we can marshal any type so the marshal function can accept any argument

func Marshal(v any) ([]byte, error) {

}

In the database/SQL package. If the query is parameterized (for example, SELECT * FROM PRODUCT WHERE id= ? ), the parameters could be any kind. Hence, it also uses any arguments:

func (c *Conn) QueryContext(ctx context.Context, query string,
args ...any) (*Rows, error) {

}

In summary, any can be helpful if there is a genuine need for accepting or returning any possible type(for instance, when it comes to marshaling or formatting). In general, we should avoid overgeneralizing the code we write at all costs. Perhaps a little bit of duplicated code might occasionally be better if it improves other aspects such as code expressiveness.

References:-

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
Previous Post
Next Post