Comprehending Go interfaces
Go provides a type called an interface that stores any value that declares a set of methods. The implementing value must have declared this set of methods to implement the interface. The value may also have other methods besides the set declared in the interface type.
If you are new to interfaces, understand that they can be a little confusing. Therefore, we will take it one step at a time.
Defining an interface type
Interfaces are most commonly defined using the type
keyword that we discussed in the earlier section on structs. The following defines an interface that returns a string representing the data:
type Stringer interface { String() string }
Note
Stringer
is a real type defined in the standard library's fmt
package. Types that implement Stringer
will have their String()
method called when passed to print
functions in the fmt
package. Don't let the similar names confuse you; Stringer
is the interface type's name, and it defines a method called String()
(which is uppercase to distinguish it from the string
type, which is lowercase). That method returns a string
type that should provide some human-readable representation of your data.
Now, we have a new type called Stringer
. Any variable that has the String()
string
method can be stored in a variable of type Stringer
. The following is an example:
type Person struct { First, Last string } func (p Person) String() string { return fmt.Sprintf("%s,%s", p.Last, p.First) }
Person
represents a record of a person, first and last name. We define String() string
on it, so Person
implements Stringer
:
type StrList []string func (s StrList) String() string { return strings.Join(s, ",") }
StrList
is a slice of strings. It also implements Stringer
. The strings.Join()
function used here takes a slice of strings and creates a single string with each entry from the slice separated by a comma:
// PrintStringer prints the value of a Stringer to stdout. func PrintStringer(s Stringer) { fmt.Println(s.String()) }
PrintStringer()
allows us to print the output of Stringer.String()
of any type that implements Stringer
. Both the types we created above implement Stringer
.
Let's see this in action:
func main() { john := Person{First: "John", Last: "Doak"} var nameList Stringer = StrList{"David", "Sarah"} PrintStringer(john) // Prints: Doak,John PrintStringer(nameList) // Prints: David,Sarah }
Without interfaces, we would have to write a separate Print[Type]
function for every type we wanted to print. Interfaces allow us to pass values that can do common operations defined by their methods.
Important things about interfaces
The first thing to note about interfaces is that values must implement every method defined in the interface. Your value can have methods not defined for the interface, but it doesn't work the other way.
Another common issue new Go developers encounter is that once the type is stored in an interface, you cannot access its fields, or any methods not defined on the interface.
The blank interface – Go's universal value
Let's define a blank interface variable: var i interface{}. i
is an interface with no defined methods. So, what can you store in that?
That's right, you can store anything.
interface{}
is Go's universal value container that can be used to pass any value to a function and then figure out what it is and what to do with it later. Let's put some things in i
:
i = 3 i = "hello world" i = 3.4 i = Person{First: "John"}
This is all legal because each of those values has types that define all the methods that the interface defined (which were no methods). This allows us to pass around values in a universal container. This is actually how fmt.Printf()
and fmt.Println()
work. Here are their definitions from the fmt
package:
func Println(a ...interface{}) (n int, err error) func Printf(format string, a ...interface{}) (n int, err error)
However, as the interface did not define any methods, i
is not useful in this form. So, this is great for passing around values, but not using them.
Note about interface{} in 1.18:
Go 1.18 has introduced an alias for the blank interface{}
, called any
. The Go standard library now uses any
in place of interface{}
. However, all packages prior to 1.18 will still use interface{}
. Both are equivalent and can be used interchangeably.
Type assertion
Interfaces can have their values asserted to either another interface type or to their original type. This is different than type conversion, where you change the type from one to another. In this case, we are saying it already is this type.
Type assertion allows us to change an interface{}
value into a value that we can do something with.
There are two common ways to do this. The first uses the if
syntax, as follows:
if v, ok := i.(string); ok { fmt.Println(v) }
i.(string)
is asserting that i
is a string
value. If it is not, ok == false
. If ok == true
, then v
will be the string
value.
The more common way is with a switch
statement and another use of the type
keyword:
switch v := i.(type) { case int: fmt.Printf("i was %d\n", i) case string: fmt.Printf("i was %s\n", i) case float: fmt.Printf("i was %v\n", i) case Person, *Person: fmt.Printf("i was %v\n", i) default: // %T will print i's underlying type out fmt.Printf("i was an unsupported type %T\n", i) }
Our default
statement prints out the underlying type of i
if it did not match any of the other cases. %T
is used to print the type information.
In this section, we learned about Go's interface
type, how it can be used to provide type abstraction, and converting an interface into its concrete type for use.