Interfaces are used in two distinct styles.
In the first style, exemplified by io.Reader
,
io.Writer
, fmt.Stringer
, sort.Interface
,
http.Handler
, and error
,
an interface’s methods express the similarities of
the concrete types that satisfy the interface but hide the
representation details and intrinsic operations of those concrete
types.
The emphasis is on the methods, not on the concrete types.
The second style exploits the ability of an interface value to hold values of a variety of concrete types and considers the interface to be the union of those types. Type assertions are used to discriminate among these types dynamically and treat each case differently. In this style, the emphasis is on the concrete types that satisfy the interface, not on the interface’s methods (if indeed it has any), and there is no hiding of information. We’ll describe interfaces used this way as discriminated unions.
If you’re familiar with object-oriented programming, you may recognize these two styles as subtype polymorphism and ad hoc polymorphism, but you needn’t remember those terms. For the remainder of this chapter, we’ll present examples of the second style.
Go’s API for querying an SQL database, like those of other languages, lets us cleanly separate the fixed part of a query from the variable parts. An example client might look like this:
import "database/sql" func listTracks(db sql.DB, artist string, minYear, maxYear int) { result, err := db.Exec( "SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?", artist, minYear, maxYear) // ... }
The Exec
method replaces each '?'
in the query string
with an SQL literal denoting the corresponding argument value, which
may be a boolean, a number, a string, or nil
.
Constructing queries this way helps avoid SQL injection attacks, in
which an adversary takes control of the query by exploiting
improper quotation of input data.
Within Exec
, we might find a function like the one below,
which converts each argument value to its literal SQL notation.
func sqlQuote(x interface{}) string { if x == nil { return "NULL" } else if _, ok := x.(int); ok { return fmt.Sprintf("%d", x) } else if _, ok := x.(uint); ok { return fmt.Sprintf("%d", x) } else if b, ok := x.(bool); ok { if b { return "TRUE" } return "FALSE" } else if s, ok := x.(string); ok { return sqlQuoteString(s) // (not shown) } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } }
A switch
statement simplifies an
if
-else
chain that performs a series of value equality
tests. An analogous type switch statement
simplifies an if
-else
chain of type assertions.
In its simplest form, a type switch looks like an ordinary switch
statement in which the operand is x.(type)
—that’s literally
the keyword type
—and each case has one or more types.
A type switch enables a multi-way branch based on the interface
value’s dynamic type.
The nil
case matches if x == nil
,
and the default
case matches if no other case does.
A type switch for sqlQuote
would have these cases:
switch x.(type) { case nil: // ... case int, uint: // ... case bool: // ... case string: // ... default: // ... }
As with an ordinary switch statement (§1.8),
cases are considered in order and,
when a match is found, the case’s body is executed. Case order
becomes significant when one or more case types are interfaces, since
then there is a possibility of two cases matching. The position of
the default
case relative to the others is immaterial.
No fallthrough
is allowed.
Notice that in the original function, the logic for the bool
and string
cases needs access to the value extracted by
the type assertion.
Since this is typical, the type switch statement has an extended form
that binds the extracted value to a new variable within each case:
switch x := x.(type) { /* ... */ }
Here we’ve called the new variables x
too; as with type
assertions, reuse of variable names is common.
Like a switch
statement, a type switch implicitly creates a
lexical block, so the declaration of the new variable called x
does not conflict with a variable x
in an outer block.
Each case
also implicitly creates a separate lexical block.
Rewriting sqlQuote
to use the extended form of type switch
makes it significantly clearer:
func sqlQuote(x interface{}) string { switch x := x.(type) { case nil: return "NULL" case int, uint: return fmt.Sprintf("%d", x) // x has type interface{} here. case bool: if x { return "TRUE" } return "FALSE" case string: return sqlQuoteString(x) // (not shown) default: panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } }
In this version, within the block of each single-type
case, the variable x
has the same type as the case.
For instance, x
has type bool
within the bool
case and string
within the string
case.
In all other cases, x
has the (interface) type of the switch
operand, which is interface{}
in this example.
When the same action is required for multiple cases, like int
and uint
, the type switch makes it easy to combine them.
Although sqlQuote
accepts an argument of any type, the
function runs to completion only if the argument’s type matches
one of the cases in the type switch; otherwise it panics with
an “unexpected type” message.
Although the type of x
is interface{}
, we consider it
a discriminated union of int
, uint
, bool
,
string
, and nil
.
3.17.6.75