Go is a statically-typed โminimalistโ programming language with an execution approach based on the notion that programs are collections of instructions to be compiled and executedโ procedural programming. As a result, it does well when writing server-side applications that are simple, fast and familiar to most programmers.
This article will look at the Reflection and Interface concepts that Go provides as part of its collection of features. Reflection gives us the flexibility to dynamically inspect and learn about the type of an arbitrary object along with information about its intrinsic structure. Interface on its own dynamics helps identify, abstract, and define behavioral patterns that different data types can share.
What is Reflection in Go?
Go uses static typing, meaning every variable has precisely one fixed type known at build time, known as its static type. With that well stated, thereโs then a concern of how to deal and interact with data types that do not yet exist when you implement your code, but may exist in the future.
In its simplest form, reflection is the capacity of a program to examine/inspect its own values and variables during execution and determine their type. A familiar scenario where reflection allows us to inspect and manipulate unknown structures is decoding JSON data. Another scenario would be when working with data types that don't implement a common interface and thus have peculiar or unknown behaviors.
Reflection is built around three concepts: Types, Kinds, and Values. The Kind can be struct, int, string, slice, map, or any other Go primitive type. If you define a struct named Foo, the Kind is a struct, and the Type is Foo. The types and functions used to implement reflection in Go reside in the standard library's reflect package. Letโs look at some helpful functions defined within the reflect package:
// go
package main
import (
"fmt"
"reflect"
)
type Foo struct {
addr string
dip Dip
}
type Dip struct {
name string
age int
}
var data = Foo{
dip: Dip{name: "Alice", age: 10},
addr: "alton street",
}
func main() {
// Inspect the reflection Type of a variable
rt := reflect.TypeOf(data)
fmt.Println(rt)
// Inspect the reflection Value of a variable
rv := reflect.ValueOf(data)
fmt.Println(rv)
// Alternatively
rt = rv.Type()
fmt.Println(rt)
// Inspect the reflection Kind of a variable
rk := rv.Kind()
fmt.Println(rk)
// Inspect the reflection Name of a variable
rn := rt.Name()
fmt.Println(rn)
// Traverse the fields of a struct.
if rt.Kind() == reflect.Struct {
for i := 0; i < rt.NumField(); i++ {
fv := rv.Field(i)
fmt.Println(fv)
}
}
// Inspect the underlying contained type in a variable
rt = reflect.TypeOf([2]int {1, 2})
ct = ct.Elem()
fmt.Println(ct)
}
Output:
// terminal
main.Foo
{alton street {Alice 10}}
main.Foo
struct
Foo
alton street
{Alice 10}
int
In the above snippet, we defined two structs, Foo
and Dip
as our sample structures to use along with the reflection
package. The reflect.TypeOf
function returns reflect.Type
, which has methods defined on its type to provide other information about the type of the variable passed in. The reflect.ValueOf
function returns reflect.Value
, which allows the reflection to read or write values.
In addition to reading, we can also use the reflection
package to modify/write the value of a structure. To modify a value using the reflect.ValueOf
function, we need to pass a pointer to the variable instead and call the Elem
function, which will return the value at the pointer address.
// go
package main
import (
"fmt"
"reflect"
)
type Pieces struct {
Id int
Content string
Rating float64
}
func main() {
data := Pieces{1, "I come in peace, comrade!", 4.1}
fmt.Printf("Original data: %+v\n", data)
mutData := reflect.ValueOf(&data).Elem()
for i := 0; i < mutData.NumField(); i++ {
if mutData.Field(i).Kind() == reflect.Int {
mutData.Field(i).SetInt(12)
} else if mutData.Field(i).Kind() == reflect.String {
mutData.Field(i).SetString("No, I live for violence, comrade!")
} else if mutData.Field(i).Kind() == reflect.Float64 {
mutData.Field(i).SetFloat(5.5)
}
}
fmt.Printf("Mutated data: %+v\n", data)
}
Output:
// terminal
Original data: {Id:1 Content:I come in peace, comrade! Rating:4.1}
Mutated data: {Id:12 Content:No, I live for violence, comrade! Rating:5.5}
In the above snippet, notice the use of the SetInt
, SetString
, and the SetFloat
functions to write the new values of the structure. The reflect
package defined more of these functions for changing the value of primitive data types. Find more info about the reflect package. Note, itโs a safe practice always to check if the value of the structure can be changed using the reflect.Value.CanSet
function.
Introduction to Type methods and Interface in Go
Type methods
When a function is attached to a specific data type, receiver argument, we consider this a type method in Go. The receiver argument gives the method the needed access to the underlying data type to either read it, write to it, or both.
We can define the type method using the following Go syntaxes:
// A copy of the receiver argument
func (p Pieces) refChange() { } // <-- 1
// a pointer to the receiver argument
func (p *Pieces) refChange() { } // <-- 2
Defining a type method with the first syntax means weโre passing a copy of the value of the receiver argument to the refChange
function. In the second definition, we defined and passed the pointer to the receiver argument instead of passing a copy of the receiver argument. This definition gives us direct access to the receiver argument in the refChange
function, meaning we can persist the changes made to the receiver argument.
Letโs implement a method on the Pieces
struct to change the values of the struct using the reflect
package:
// go
...
func (p *Pieces) refChange(id int64, content string, rating float64) {
mutData := reflect.ValueOf(p).Elem()
for i := 0; i < mutData.NumField(); i++ {
if mutData.Field(i).Kind() == reflect.Int {
mutData.Field(i).SetInt(id)
} else if mutData.Field(i).Kind() == reflect.String {
mutData.Field(i).SetString(content)
} else if mutData.Field(i).Kind() == reflect.Float64 {
mutData.Field(i).SetFloat(rating)
mutData.Field(i).CanSet()
}
}
}
func main() {
data := &Pieces{1, "I come in peace, comrade!", 4.1}
fmt.Printf("Original data: %+v\n", data)
data.refChange(100, "No, peace is not an option comrade!", 4.7)
fmt.Printf("Mutated data: %+v\n", data)
}
Output:
// terminal
Original data: {Id:1 Content:I come in peace, comrade! Rating:4.1}
Mutated data: {Id:100 Content:No, peace is not an option comrade! Rating:4.7}
This is a simple introduction to the type method on structures in Go. Learn more about the type method in the Go documentation.
Interfaces
In Go, an interface is a mechanism for defining behavior that is implemented using a set of method signatures. The interface type describes the behavioral expectation of other types by defining a set of type methods that need to be implemented by these other types before claiming to support the behavior. In essence, interface works with type methods associated with a given data type; although we can use any data type in Go, itโs usually struct. So, before a Go type satisfies an interface type, it would need to implement all of the type methods defined by the interface type.
...
type Blog interface {
ReadContent(string) (string, int)
GetRating() (float64)
}
...
The above snippet defines an interface type with two function signatures, ReadContent
and GetRating
. For a type in Go to implement this interface means it has ReadContent
and GetRating
type methods defined. Letโs define these type methods for the Pieces
struct:
...
func (p *Pieces) ReadContent(input string) (string, int) {
if input != "" {
p.Content = input
}
return p.Content, utf8.RuneCountInString(p.Content)
}
func (p *Pieces) GetRating() (float64) {
return p.Rating
}
...
In the above snippet, once we implemented the functions of an interface for the Pieces
data type, that interface is satisfied automatically for that data type. To make some sense out of these, letโs say we want to publish any post on our blog, but we want the post to be readable and rate-able. With the help of interface type, we can place some sort of boundary on a Publish
function:
...
func Publish(post Blog) {
if r := post.GetRating(); r != 0 {
c, rc := post.ReadContent("")
fmt.Printf("New post: %s\nWord count:%d\tWith rating: %.1f\n", c, rc, r)
}
}
...
Output:
// terminal
New post: No, peace is not an option comrade!
Word count:35 With rating: 4.7
Interfaces are abstract types that outline a set of methods that must be implemented for another type before it can be regarded as an instance of the interface. In other words, an interface consists of both a type and a collection of methods.
An empty interface is defined as just interface{}
. Since an empty interface has no methods, it can and is already implemented by every data type. For example, a slice which defines its elements as a type of empty interface means it can store anything in itself.
// go
s := []interface{}{4, "string", 2.5, new(Pieces), true} // [4 string 2.5 0x140000ae000 true]
Itโs advised to sparingly make use of the empty interface, especially when storing sequences of data so as to avoid errors that are easily tracked down during compiling time.
Letโs briefly look at three commonly used interfaces from the standard library shipped with Go upon installation. The three interfaces are the sort.Interface
, the io.Writer
, and io.Reader
interfaces. Letโs briefly explain each below:
- The
sort.Interface
: Thesort.Interface
defines three necessary methods for custom implementation to sort a slice of custom data. Thesort
package contains thesort.Interface
interface with the below definition:
// go
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
Once we implement sort.Interface
for our custom data stored in slices, the above three methods allow us to sort the slices according to our custom needs and data.
- The
Len
function returns the actual length of the slice being sorted. - The
Less
function contains the implementation of how the elements would be compared and sorted; this implementation would be custom to the needs of the developer but must return the boolean value of the comparison between the element at indexi
andj
. -
The
Swap
does the actual swapping of elements within the slice based on the outcome ofLess
. This is required for the sorting algorithm to work.2 . The
io.Writer
: This interface represents the writing part of the basis of file I/O in Go. The operation of writing to either a file, network or any writable process, involves the implementation ofio.Writer
for that writable object (data type). The interface definition is:
// go
type Writer interface {
Write(p []byte) (n int, err error)
}
The above interface definition needs only one function signature to satisfy the Writer
interface. The Write
function takes as its argument a byte slice containing the data we want to write to, either a file, network or any writable object, and returns two named values, n
and err
. These values represent the number of bytes written and an error
variable.
- The
io.Reader
: This interface represents the reading part of the basis of file I/O in Goโthe operation of reading from either a file, network or any readable process. TheReader
interface defines only one function signature just likeio.Writer
. Letโs look at the interface definition:
// go
type Reader interface {
Read(p []byte) (n int, err error)
}
The above definition takes as its argument a byte slice which we will fill with data being read from a file, a network, or any readable object. We fill the byte slice with data up to its length. The return values represent the number of bytes read and an error
variable.
Use cases of Go interface
The concept of interfaces is predominantly used across different Go packages and executable programs to achieve flexibility without affecting the program's performance. The following example will explore the use of sort.Interface
to sort the Pieces
struct type:
// go
...
type PSlice []Pieces
func (ps PSlice) Len() int {
return len(ps)
}
func (ps PSlice) Less(i, j int) bool {
return ps[i].Id < ps[j].Id
}
func (ps PSlice) Swap(i, j int) {
ps[i], ps[j] = ps[j], ps[i]
}
...
func main() {
posts := []Pieces {
{7, "I come in peace, comrade!", 4.1},
{100, "No, peace is not an option comrade!", 4.7 },
{13, "I preach violence, comrade!", 4.5},
{-1, "I live for violence, comrade", 5.5},
}
fmt.Println("Original:", posts)
sort.Sort(PSlice(posts))
fmt.Println("Sorted:", posts)
// Reverse sorting automatically works
sort.Sort(sort.Reverse(PSlice(posts)))
fmt.Println("Reverse sort:", posts)
...
}
Output:
Original: [{7 I come in peace, comrade! 4.1} {100 No, peace is not an option comrade! 4.7} {13 I preach violence, comrade! 4.5} {-1 I live for violence, comrade 5.5}]
Sorted: [{-1 I live for violence, comrade 5.5} {7 I come in peace, comrade! 4.1} {13 I preach violence, comrade! 4.5} {100 No, peace is not an option comrade! 4.7}]
Reverse sort: [{100 No, peace is not an option comrade! 4.7} {13 I preach violence, comrade! 4.5} {7 I come in peace, comrade! 4.1} {-1 I live for violence, comrade 5.5}]
Thatโs a long example, but only implemented the foundational concepts we shared earlier about the sort.Interface
interface. We implemented Len
, Less
and Swap
for a type, PSlice
, which is basically a slice of Pieces
. With the type methods in place, we can now use the sort.Sort
function to sort the slice, and also make use of the sort.Reverse function to perform reverse sorting on the slice. The PSlice(posts)
expression will adapt the []Pieces
slice into the PSlice
type with the methods Len
, Less
and Swap
. This is possible because []Pieces
has the appropriate type signature with PSlice
.
Conclusion
In this tutorial, we covered what reflection and interfaces are in Go. For example, reflection provides a more elegant approach to scrubbing sensitive data before logging it. And as well, interfaces provide the flexibility to implement functionalities based on behaviors expected on a data type. We can now extend these concepts to build complex interfaces by combining one interface with another to form a new interface. The ReadCloser
interface is a typical example of combining two interfaces, Reader
and Closer
. The extensibility of these concepts are far beyond this tutorial, so I encourage you to explore more on these concepts. For more about when to use Go in general, check out this comparison guide.