Go 1.18 Generics
With the release of Go 1.18 Generics were added to the language, a major update to the language and with it comes new concepts and syntax. In this article we take a look at what Generics are and how they can be used, refactoring a HTTP server package to make use of the benefits Generics provide.
What are Generics
Generic programming is a style of which a type can be instantiated when being used. This enables many benefits including, stronger types, no type casting and provides reusable solutions.
Generic Types
A Generic is identified by the generic type parameters, these are wrapped around square brackets and are provided after the declared name.
package main
// GenericServer with a Generic type parameter T http.Handler
type GenericServer[T http.Handler] struct {
Handler T
}
In this given example http.Handler
is an interface from the http package,
our Generic server type parameter T
can be instantiated with any type that conforms to the built-in Handler
interface,
providing access to all the methods of that type.
Comparing that to a non-generic implementation, we provide the http interface directly to the Handler
field.
package main
type Server struct {
Handler http.Handler
}
The non-generic type conforms to the underlying http interface which contains one method,
ServeHTTP(ResponseWriter, *Request)
but does not get any other methods for the given type.
Using the Gin framework as an example, we can provide the gin.Engine
type and
get the strong typing that provides access to all the Gin methods.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type GenericServer[T http.Handler] struct {
Handler T
}
func main() {
genericServer := GenericServer[*gin.Engine]{}
genericServer.Handler.GET("/foo", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, nil)
})
genericServer.Handler.POST("/foo", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, nil)
})
}
Both GET
and POST
are only available to the Gin web framework, since we provide the gin.Engine
as the type parameter,
the compiler can interpret the methods by the instantiated type.
In the non-generic implementation, the only method we have access to is the one
declared in the interface ServeHTTP(ResponseWriter, *Request)
.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Server struct {
Handler http.Handler
}
func main() {
server := Server{
Handler: gin.New(),
}
// Unresolved reference 'GET'
server.Handler.GET("/foo", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, nil)
})
}
Trying to access the Gin specific types result in a compilation error, even though
the method exists in Gin, we receive an unresolved reference as the gin.Engine
type is not instantiated.
Generic Functions
Generic functions are similar to the types in syntax, as well as providing us a way of initialising Generics.
package main
func NewGenericServer[T http.Handler](handler T) *GenericServer[T] {
return &GenericServer[T]{
Handler: handler,
}
}
We define a function with the generic type parameters and use the parameter T
as the handler
arguments type. This
enables us to initialise the GenericServer
struct with a given argument of any type that conforms to http.Handler
.
The same example as a non-generic function would provide the http.Handler
type on the argument.
package main
func NewServer(handler http.Handler) *Server {
return &Server{
Handler: handler,
}
}
With the function accepting a type parameter, we can instantiate the function with the gin.Engine
type parameter and
make use of its methods.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type GenericServer[T http.Handler] struct {
Handler T
}
func NewGenericServer[T http.Handler](handler T) *GenericServer[T] {
return &GenericServer[T]{
Handler: handler,
}
}
func main() {
router := gin.New()
genericServer := NewGenericServer[*gin.Engine](router)
genericServer.Handler.GET("/foo", func(context *gin.Context) {
context.JSON(http.StatusOK, nil)
})
}
In comparison, the same example as a non-generic function would pass the argument types with the arguments but no types can be instantiated, therefore we receive an unresolved reference at compile time.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Server struct {
Handler http.Handler
}
func NewServer(handler http.Handler) *Server {
return &Server{
Handler: handler,
}
}
func main() {
router := gin.New()
server := NewServer(router)
// Unresolved reference 'GET'
server.Handler.GET("/foo", func(context *gin.Context) {
context.JSON(http.StatusOK, nil)
})
}
Generics
Generics are a powerful feature that provide the ability to instantiate types at compile time, enabling the reuse of types and functions that can be used with improved stronger typing without the need for casting.