AlgoMaster Logo

Interfaces Basics

Last Updated: May 22, 2026

High Priority
9 min read

An interface in Go is a named set of method signatures. Any concrete type that has all those methods can be used wherever the interface is expected, which is how Go lets one piece of code work with many different types without knowing their concrete details. This lesson covers what an interface is, how to declare one, how a struct provides the required methods, how to use an interface as a parameter or variable type, and the high-level shape of an interface value at runtime.

What an Interface Actually Is

An interface is a contract written purely in terms of behavior. It says: "any value of this type must have these methods, with these signatures." It does not say what those methods do, what fields the underlying value has, or how it stores its data. Only the method set.

Take an online store that accepts multiple ways to pay: a credit card, a PayPal account, store credit, a gift card. Each one is a different concrete type with different fields, but they all need to do the same thing at checkout: charge an amount and report whether it worked. That shared capability is what an interface captures.

The type Payable interface {... } declaration creates a new named type called Payable. From now on, Payable is a type you can use like int or string: in variable declarations, function parameters, return values, struct fields. A variable of type Payable can hold any concrete value whose type has a Pay(amount float64) error method, and nothing else. With no value assigned, an interface variable starts as nil, which is its zero value.

The key shift from earlier sections is that an interface doesn't describe data, it describes capability. A Product struct says "I have a Code, a Name, a Price, a Stock". A Payable interface says "I can be paid with". One is a shape, the other is a verb.

Declaring an Interface

The declaration syntax mirrors type... struct: you write type, the name, the keyword interface, and a brace-enclosed list of method signatures.

Each method signature lists the method name, the parameter list, and the result list. There's no func keyword, no body, and no receiver. That makes sense, because an interface doesn't implement anything; it just describes what implementers must provide. Method names are exported when they start with a capital letter, exactly like every other identifier in Go.

Interface names tend to follow a convention: single-method interfaces are often named after the method with -er on the end (Reader, Writer, Stringer, Payable). Multi-method interfaces are named for the role they describe (http.Handler, sort.Interface). The convention isn't enforced by the compiler, but it's worth following because the rest of the standard library uses it.

You can declare an interface anywhere a type can be declared: at the package level, where it's visible across the whole package, or inside a function, where it's visible only within that function. Package-level is far more common.

Implementing an Interface

A concrete type implements an interface when its method set includes every method the interface requires, with matching signatures. The signatures have to match exactly: same method name, same parameter types in the same order, same result types. There is no implements keyword, no : syntax, no explicit declaration that "this type implements this interface". The match happens automatically based on the methods that exist.

Here's Payable with two concrete types implementing it:

Both *CreditCardPayment and *PayPalPayment have a method named Pay that takes a float64 and returns an error. That's all Payable requires, so both types satisfy the interface. Neither type had to declare anything special. The compiler matched the methods to the interface automatically.

The methods use pointer receivers (func (c *CreditCardPayment) Pay(...) and func (p *PayPalPayment) Pay(...)). That's because each Pay call mutates the Balance field, which has to happen on the original struct, not a copy. The choice between value and pointer receivers was covered in the value vs pointer receivers chapter, and it directly affects which type (the struct or the pointer to the struct) ends up satisfying an interface. We'll come back to method sets in detail in the method sets and implicit interface satisfaction chapters, but for now: when in doubt, use a pointer receiver for methods that change the struct.

A read-only method works with a value receiver, and the value type itself satisfies the interface:

Because Price has a value receiver and doesn't mutate Book, the value type Book (not just *Book) satisfies the Priceable interface. We'll dig into the rules in the *Method Sets and Implicit Interface Satisfaction* chapters.

Using an Interface as a Variable or Parameter

The whole point of an interface is that code written against the interface works with every type that satisfies it. You write a function that takes a Payable, and the same function works for credit cards, PayPal accounts, gift cards, anything that comes along later with a matching Pay method.

The checkout function declares its first parameter as Payable. It doesn't know or care whether the argument is a credit card, a gift card, or something else. It only knows it can call Pay on it. The caller passes whichever concrete value they want, and Go takes care of dispatching the method call to the right concrete Pay at runtime.

The same idea works for variables. An interface variable can hold a value of any type that satisfies the interface, and you can reassign it to a different concrete type later:

The variable method is declared once as a Payable. The first assignment puts a credit card in it; the second swaps in a store credit balance. Both calls to method.Pay go through the same interface, but they dispatch to different concrete methods. The compiler accepts both assignments because both *CreditCardPayment and *StoreCreditPayment have the right method.

Why Interfaces Are Useful

Two payoffs justify the indirection that interfaces add. The first is polymorphism: one piece of code works with many types. The second is decoupling: code that depends on an interface doesn't depend on any particular concrete type, so the types can change without touching the dependent code.

A checkout function that works with Payable keeps working when someone adds a new payment method. The new method just has to provide a Pay method with the right signature; no change to checkout is needed.

The checkout function works with a slice of Payable values and splits the total evenly. Three different concrete types live in that slice. The function loops over them and calls Pay on each one through the interface. When ApplePayPayment was added, checkout didn't change.

That's polymorphism: the same call site dispatches to different concrete methods at runtime based on the actual type behind the interface. And it's decoupling: checkout only knows about Payable, so the concrete payment types can be replaced, refactored, or extended without rippling through the checkout code.

A second common use is testing. Production code can take a real database connection or HTTP client; tests can pass a fake that implements the same interface. Because the function under test only sees the interface, the swap is invisible to it.

cartTotal is written against the PriceLookup interface. In a test you pass FakePriceLookup with a fixed map. In production you pass a real implementation that reads from a database. The function itself doesn't change, because it only sees the interface.

The diagram shows the one-to-many shape that interfaces create. One interface type (Payable) sits in the middle. Many concrete types feed into it by providing the required method. Code that consumes the interface (checkout) only knows about the interface, so adding a new concrete type on the left doesn't require any change on the right.

What an Interface Value Looks Like Inside

At runtime, an interface value isn't a copy of the concrete value. It's a small two-word structure that holds two pointers: one to the dynamic type's metadata (so Go knows which methods to dispatch to) and one to the underlying concrete value (or to a pointer to it). The Go runtime calls these the "type word" and the "value word".

The implication is that an interface value carries both a type and a value at the same time. When you call a method on the interface, the runtime looks at the type word to find the right method and passes the value word as the receiver.

The interface value method Payable holds two words. The first identifies the concrete type, here *CreditCardPayment. The second points at (or contains) the concrete value, here a CreditCardPayment struct sitting elsewhere in memory. When method.Pay(75.0) runs, the runtime uses the type word to look up *CreditCardPayment.Pay and calls it with the value word as the receiver.

an interface method call goes through an extra lookup compared to a direct method call on a concrete type. For most code, the difference is invisible. In a tight loop calling millions of times, it can show up in a profile, and the fix is usually to call the concrete type directly inside the hot path.

Two interface values compare equal if and only if both their type words and value words match. Comparing a Payable holding a *CreditCardPayment to a Payable holding a *PayPalPayment is false regardless of any field values, because the dynamic types differ.

Nil Interfaces and the "Interface Holding Nil" Trap

The zero value of an interface is nil. A var method Payable with no assignment has both its type word and its value word set to nil, and method == nil is true. Calling a method on a nil interface panics.

That's straightforward. The trap is the case where the interface variable is not nil, but the concrete value inside it is nil. For example:

The pointer card is nil. Assigning it to method puts a value into the interface: the type word becomes *CreditCardPayment, and the value word becomes the nil pointer. The interface is not nil, because its type word is set. Comparing method == nil returns false, which is the source of many real-world Go bugs.

The deeper rules around this case (when to return a typed nil from a function, why returning a nil interface is different from returning a nil pointer assigned to one) are covered in the Pointers and Error Handling sections. For now, the takeaway is simpler: an interface is nil only when both its type word and its value word are nil. An interface holding any typed value, even a typed nil, compares as non-nil.

A Working End-to-End Example

that ties the pieces together. It defines a Payable interface, two concrete payment types, and a processOrder function written against the interface. The function works for both types without knowing anything about either one beyond the interface.

processOrder accepts any Payable. The first call works because the card has enough balance; the second fails because the same card now only has $25 left. The third uses the PayPal account, which is short on funds. The function doesn't know the difference between these types and doesn't need to.

This is what most code that uses interfaces looks like: declare the interface for the smallest set of behavior the consuming code needs, write the consumer against the interface, and let any number of concrete types plug in. The other chapters in this section explore the edges: how satisfaction works in detail, when to use the empty interface, how to dig out the concrete type when you need to, and where interfaces fit in the design of larger systems.