Interface #
Interfaces in Go are the feature that most confuses developers coming from Java or C# — not because they’re hard, but because they’re far simpler than expected. In Java, you must explicitly declare implements Runnable. In Go, there’s no such declaration at all. A type automatically satisfies an interface if it has all the methods the interface requires. This isn’t a design flaw — it’s the greatest strength of Go interfaces, making decoupling between components feel completely natural and making testing much easier.
Interfaces Define Behavioral Contracts #
Interfaces in Go only define what can be done — not what is held. No fields, no implementation, just a list of method signatures:
// The interface defines a contract: "anything that can be written to"
type Writer interface {
Write(p []byte) (n int, err error)
}
// The interface defines a contract: "anything that can be closed"
type Closer interface {
Close() error
}
// The interface defines a contract: "anything that has a string representation"
type Stringer interface {
String() string
}
// A richer interface
type Shape interface {
Area() float64
Perimeter() float64
String() string
}
Notice there’s no public, abstract, or virtual keyword. Just a list of methods and their types.
Implicit Implementation — Go’s Greatest Strength #
In Java: class MyWriter implements Writer { ... } — you must declare that you implement an interface.
In Go: there’s no declaration at all. The compiler checks by itself whether a type satisfies all the methods the interface requires:
type Writer interface {
Write(p []byte) (n int, err error)
}
// FileWriter implements Writer — without "implements Writer"!
type FileWriter struct {
path string
f *os.File
}
func (fw *FileWriter) Write(p []byte) (int, error) {
return fw.f.Write(p)
}
// NetworkWriter also implements Writer — same, no declaration
type NetworkWriter struct {
conn net.Conn
}
func (nw *NetworkWriter) Write(p []byte) (int, error) {
return nw.conn.Write(p)
}
// This function accepts ANYTHING that can be written to
func saveData(w Writer, data []byte) error {
_, err := w.Write(data)
return err
}
func main() {
fw := &FileWriter{path: "output.txt"}
nw := &NetworkWriter{}
saveData(fw, []byte("data to file")) // ✓
saveData(nw, []byte("data to network")) // ✓ — polymorphism!
}
Why Implicit Implementation Is So Powerful #
Imagine you’re using a third-party library that defines a struct ExternalDB. That library knows nothing about the UserRepository interface you created. But as long as ExternalDB has the methods your interface needs, it automatically satisfies it — without you having to modify the library, without needing a wrapper class.
// Your interface in the application package
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// A third-party library — you can't change this code,
// but it has FindByID and Save methods
type ThirdPartyDB struct { ... }
func (db *ThirdPartyDB) FindByID(id int) (*User, error) { ... }
func (db *ThirdPartyDB) Save(user *User) error { ... }
// ThirdPartyDB automatically satisfies UserRepository
// without modifying ThirdPartyDB at all!
var repo UserRepository = &ThirdPartyDB{}
Interface Values — Two Internal Components #
It’s important to understand: an interface variable stores two things at once — the concrete type and its concrete value. The internal structure of this interface value can be visualized in the following diagram:
flowchart TD
subgraph InterfaceValue["Interface Value (Internal Structure)"]
Type["Dynamic Type (T)<br>Stores the concrete type (e.g. *FileWriter)"]
Value["Concrete Value (V)<br>Points to the concrete value/data"]
endThis affects how nil interfaces work:
var w Writer // type=nil, value=nil → nil interface
var fw *FileWriter = nil
w = fw // type=*FileWriter, value=nil → interface is NOT nil!
fmt.Println(w == nil) // false! — even though fw is a nil pointer
This is a very famous gotcha in Go — a nil interface is different from an interface holding a nil pointer:
// ANTI-PATTERN: this function doesn't behave as expected
func getWriter(useFile bool) Writer {
var fw *FileWriter // nil pointer
if useFile {
fw = openFile()
}
return fw // returns a Writer holding (*FileWriter, nil)
// not a nil Writer!
}
func main() {
w := getWriter(false)
if w == nil {
fmt.Println("no writer") // NEVER printed!
}
// w != nil even though fw is a nil pointer
// Calling w.Write() will panic because fw is nil
}
// CORRECT: return a nil interface explicitly
func getWriter(useFile bool) Writer {
if useFile {
return openFile() // a valid *FileWriter
}
return nil // the real nil interface
}
Never return a possibly-nil interface variable from a function. Always returnnilexplicitly when there’s no implementation. Returning(*ConcreteType)(nil)wrapped in an interface produces a non-nil interface, makingif result == nilchecks always false.
Small Interfaces Are More Powerful #
One of the most important principles in the Go community: a small interface is more useful than a large one. io.Reader has only one method — yet it’s used in thousands of places across the Go ecosystem:
// io.Reader — one method, thousands of implementations
type Reader interface {
Read(p []byte) (n int, err error)
}
// All of these implement io.Reader:
// - *os.File
// - *bytes.Buffer
// - *strings.Reader
// - net.Conn
// - *http.Request.Body
// - *gzip.Reader
// - *zip.Reader
// ...and hundreds more
// A function accepting io.Reader works with ALL implementations above
func countLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Can be used with files, strings, network connections, etc.
lines, _ := countLines(os.Stdin)
lines, _ = countLines(strings.NewReader("line 1\nline 2\n"))
lines, _ = countLines(httpResp.Body)
Interface size guide:
1-2 methods ✓ Excellent — implementable by many types
3-5 methods ✓ Still OK — a clear, bounded contract
6-10 methods ⚠ Getting heavy — consider splitting into small interfaces
10+ methods ✗ Almost certainly too large — hard to mock, hard to use
Interface Composition #
Interfaces can be embedded into other interfaces to form larger contracts, exactly like struct embedding:
// Atomic interfaces — one capability each
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
// Composition — built from smaller interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
type ReadWriteSeeker interface {
Reader
Writer
Seeker
}
// A type implementing ReadWriteCloser automatically
// also implements Reader, Writer, and Closer separately
This composition lets you choose the right contract for each function — give only what’s needed, nothing more:
func processInput(r Reader) { ... } // only needs Read
func writeOutput(w Writer) { ... } // only needs Write
func handleConn(rwc ReadWriteCloser) { ... } // needs all three
any and interface{}
#
interface{} (or its alias any since Go 1.18) is the empty interface — it satisfies every type because it requires no methods at all:
// any and interface{} are identical — any is just an alias
var v any = 42
v = "hello"
v = []int{1, 2, 3}
v = struct{ X int }{X: 10}
// Useful for generic containers before generics (Go 1.18)
type Stack struct {
items []any
}
func (s *Stack) Push(item any) {
s.items = append(s.items, item)
}
func (s *Stack) Pop() (any, bool) {
if len(s.items) == 0 {
return nil, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
Use
anyvery sparingly. Every time you useany, you lose the type safety Go provides — the compiler can’t help detect type errors at compile time. Since Go 1.18, use generics as the type-safe alternative for generic containers:// ANTI-PATTERN: loses type safety func contains(slice []any, item any) bool { ... } items := []any{1, 2, 3} // CORRECT since Go 1.18: type-safe with generics func contains[T comparable](slice []T, item T) bool { for _, v := range slice { if v == item { return true } } return false } fmt.Println(contains([]int{1, 2, 3}, 2)) // true, type-safe fmt.Println(contains([]string{"a", "b"}, "c")) // false, type-safe
Type Assertions #
A type assertion extracts the concrete value from an interface variable:
var i interface{} = "Hello, Go!"
// Safe form — always use this
str, ok := i.(string)
if ok {
fmt.Println(str) // Hello, Go!
fmt.Println(len(str)) // 10
} else {
fmt.Println("not a string")
}
// Unsafe form — PANICS if the type doesn't match
str2 := i.(string) // OK because i is indeed a string
num := i.(int) // PANIC: interface conversion: interface {} is string, not int
Type assertions are very useful for checking extra capabilities of a value received as an interface:
type Writer interface {
Write([]byte) (int, error)
}
// Check whether a writer can also be closed
func writeAndClose(w Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Type assertion to check for a Close capability
if closer, ok := w.(io.Closer); ok {
return closer.Close() // call Close if available
}
return nil // fine if there's no Close
}
// Check whether an error carries extra information
func handleError(err error) {
// Check whether the error is a specific type
var netErr *net.OpError
if errors.As(err, &netErr) {
fmt.Println("network error on operation:", netErr.Op)
return
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("path error on:", pathErr.Path)
return
}
fmt.Println("generic error:", err)
}
Type Switches #
A type switch is an elegant way to handle various possible types in one block — much cleaner than a series of type assertions:
func formatValue(v any) string {
switch val := v.(type) {
case nil:
return "<nil>"
case bool:
if val {
return "true"
}
return "false"
case int:
return strconv.Itoa(val)
case int64:
return strconv.FormatInt(val, 10)
case float64:
return strconv.FormatFloat(val, 'f', -1, 64)
case string:
return fmt.Sprintf("%q", val)
case []byte:
return fmt.Sprintf("bytes(%d)", len(val))
case error:
return "error: " + val.Error()
case fmt.Stringer:
// A type implementing Stringer
return val.String()
default:
return fmt.Sprintf("%T(%v)", val, val)
}
}
func main() {
fmt.Println(formatValue(nil)) // <nil>
fmt.Println(formatValue(true)) // true
fmt.Println(formatValue(42)) // 42
fmt.Println(formatValue(3.14)) // 3.14
fmt.Println(formatValue("hello")) // "hello"
fmt.Println(formatValue([]byte{1,2})) // bytes(2)
}
Interfaces for Dependency Injection and Testing #
This is the most important use of interfaces in production code. By defining dependencies as interfaces, you can:
- Swap implementations without changing the code that uses them
- Inject mocks during testing without any external library
// Define dependencies as interfaces on the consumer side
type EmailSender interface {
Send(to, subject, body string) error
}
type SMSSender interface {
Send(to, message string) error
}
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// The service depends on interfaces, not concrete implementations
type UserService struct {
repo UserRepository
email EmailSender
sms SMSSender
}
func NewUserService(repo UserRepository, email EmailSender, sms SMSSender) *UserService {
return &UserService{repo: repo, email: email, sms: sms}
}
func (s *UserService) Register(name, emailAddr, phone string) error {
user := &User{Name: name, Email: emailAddr, Phone: phone}
if err := s.repo.Save(user); err != nil {
return fmt.Errorf("failed to save user: %w", err)
}
// Send notifications — doesn't care about the implementation
if err := s.email.Send(emailAddr, "Welcome!", "Your account was created successfully."); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
// ── Real implementations for production ────────────────────────
type SMTPEmailSender struct {
host string
port int
}
func (s *SMTPEmailSender) Send(to, subject, body string) error {
// send email via SMTP
fmt.Printf("[SMTP] Sending to %s: %s\n", to, subject)
return nil
}
type TwilioSMSSender struct {
apiKey string
}
func (t *TwilioSMSSender) Send(to, message string) error {
// send SMS via the Twilio API
return nil
}
// ── Mocks for testing — without any framework! ─────────────
type MockEmailSender struct {
SentEmails []struct{ To, Subject, Body string }
ShouldFail bool
}
func (m *MockEmailSender) Send(to, subject, body string) error {
if m.ShouldFail {
return errors.New("mock: email failed to send")
}
m.SentEmails = append(m.SentEmails, struct{ To, Subject, Body string }{to, subject, body})
return nil
}
type MockUserRepo struct {
Users map[int]*User
SaveErr error
}
func (r *MockUserRepo) FindByID(id int) (*User, error) {
user, ok := r.Users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
return user, nil
}
func (r *MockUserRepo) Save(user *User) error {
if r.SaveErr != nil {
return r.SaveErr
}
if r.Users == nil {
r.Users = make(map[int]*User)
}
r.Users[len(r.Users)+1] = user
return nil
}
Interfaces on the Consumer Side, Not the Producer Side #
This is the most important design principle that’s often violated:
// ANTI-PATTERN: interface defined on the PRODUCER side
// package userservice
type UserServiceInterface interface {
CreateUser(name, email string) (*User, error)
GetUser(id int) (*User, error)
UpdateUser(id int, data UpdateData) error
DeleteUser(id int) error
ListUsers(filter Filter) ([]*User, error)
// ... 10 more methods
}
type UserService struct { ... }
// UserService implements UserServiceInterface
// Problem: a consumer that only needs GetUser is forced to depend
// on this giant interface, and its mock must implement all
// 15 methods even though only 1 is used
// ─────────────────────────────────────────────────────────────
// CORRECT: interfaces defined on the CONSUMER side
// package handler — only define what's needed
type UserGetter interface {
GetUser(id int) (*User, error)
}
type ProfileHandler struct {
users UserGetter // small interface, easy to mock
}
// package ordersvc — different needs, different interface
type UserValidator interface {
GetUser(id int) (*User, error)
}
type OrderService struct {
users UserValidator
}
The result: UserService (the real struct) automatically satisfies both UserGetter and UserValidator because both only need the same single method. Each consumer gets the smallest interface they need.
Method Sets — Value vs Pointer #
There’s an important rule about method sets that determines when value and pointer types can satisfy an interface:
type Animal interface {
Sound() string
Move()
}
type Dog struct{ Name string }
func (d Dog) Sound() string { return "Woof" } // value receiver
func (d *Dog) Move() { fmt.Println(d.Name, "is running") } // pointer receiver
func main() {
// *Dog implements Animal — the pointer has ALL methods
var a Animal = &Dog{Name: "Buddy"} // ✓
a.Sound()
a.Move()
// Dog does NOT implement Animal — a value doesn't have pointer methods
// var b Animal = Dog{Name: "Buddy"} // ✗ compile error:
// Dog does not implement Animal (Move method has pointer receiver)
}
Method Set Rules:
Type T has methods:
→ All methods with VALUE receivers (T)
Type *T has methods:
→ All methods with VALUE receivers (T)
→ All methods with POINTER receivers (*T)
Implications for interfaces:
→ If the interface has a method with a pointer receiver,
only *T can satisfy the interface, not T
→ Use a pointer (&value) when assigning to an interface if
there are methods with pointer receivers
Complete Example Program #
The following program builds a multi-channel notification system demonstrating dependency injection via interfaces:
package main
import (
"fmt"
"strings"
"time"
)
// ── Interface Definitions ─────────────────────────────────────
type Notifier interface {
Send(recipient, message string) error
Name() string
}
type NotificationStore interface {
Save(n Notification) error
FindByRecipient(recipient string) []Notification
}
// ── Domain Types ──────────────────────────────────────────────
type Priority int
const (
PriorityLow Priority = iota
PriorityNormal
PriorityHigh
PriorityCritical
)
func (p Priority) String() string {
switch p {
case PriorityLow: return "Low"
case PriorityNormal: return "Normal"
case PriorityHigh: return "High"
case PriorityCritical: return "Critical"
default: return "Unknown"
}
}
type Notification struct {
ID int
Recipient string
Message string
Channel string
Priority Priority
SentAt time.Time
Success bool
Error string
}
// ── Concrete Notifier Implementations ────────────────────────
type EmailNotifier struct {
SMTPHost string
From string
sentCount int
}
func (e *EmailNotifier) Send(recipient, message string) error {
// Simulate sending an email
e.sentCount++
fmt.Printf(" 📧 [EMAIL] To: %s\n %s\n", recipient, message)
return nil
}
func (e *EmailNotifier) Name() string { return "Email" }
type SlackNotifier struct {
WebhookURL string
Channel string
}
func (s *SlackNotifier) Send(recipient, message string) error {
fmt.Printf(" 💬 [SLACK] #%s @%s: %s\n", s.Channel, recipient, message)
return nil
}
func (s *SlackNotifier) Name() string { return "Slack" }
type SMSNotifier struct {
APIKey string
FromNum string
}
func (s *SMSNotifier) Send(recipient, message string) error {
// SMS messages are usually length-limited
if len(message) > 160 {
message = message[:157] + "..."
}
fmt.Printf(" 📱 [SMS] To: %s | %s\n", recipient, message)
return nil
}
func (s *SMSNotifier) Name() string { return "SMS" }
// ── In-Memory Store ───────────────────────────────────────────
type InMemoryStore struct {
notifications []Notification
nextID int
}
func (s *InMemoryStore) Save(n Notification) error {
s.nextID++
n.ID = s.nextID
s.notifications = append(s.notifications, n)
return nil
}
func (s *InMemoryStore) FindByRecipient(recipient string) []Notification {
var result []Notification
for _, n := range s.notifications {
if n.Recipient == recipient {
result = append(result, n)
}
}
return result
}
// ── Notification Service ──────────────────────────────────────
type NotificationService struct {
notifiers map[string]Notifier
store NotificationStore
}
func NewNotificationService(store NotificationStore) *NotificationService {
return &NotificationService{
notifiers: make(map[string]Notifier),
store: store,
}
}
func (ns *NotificationService) Register(notifier Notifier) {
ns.notifiers[notifier.Name()] = notifier
}
func (ns *NotificationService) Notify(
recipient, message string,
priority Priority,
channels ...string,
) {
// Determine channels based on priority if not specified
if len(channels) == 0 {
switch priority {
case PriorityCritical:
channels = []string{"Email", "SMS", "Slack"}
case PriorityHigh:
channels = []string{"Email", "Slack"}
default:
channels = []string{"Email"}
}
}
fmt.Printf("\n[%s] Sending to %s (Priority: %s):\n",
time.Now().Format("15:04:05"), recipient, priority)
for _, ch := range channels {
notifier, ok := ns.notifiers[ch]
if !ok {
fmt.Printf(" ⚠ Channel %q is not registered\n", ch)
continue
}
n := Notification{
Recipient: recipient,
Message: message,
Channel: ch,
Priority: priority,
SentAt: time.Now(),
}
err := notifier.Send(recipient, message)
if err != nil {
n.Error = err.Error()
fmt.Printf(" ✗ Failed to send via %s: %v\n", ch, err)
} else {
n.Success = true
}
_ = ns.store.Save(n)
}
}
func (ns *NotificationService) History(recipient string) {
notifications := ns.store.FindByRecipient(recipient)
if len(notifications) == 0 {
fmt.Printf("\nNo notification history for %s\n", recipient)
return
}
fmt.Printf("\n=== Notification History: %s ===\n", recipient)
for _, n := range notifications {
status := "✓"
if !n.Success {
status = "✗"
}
fmt.Printf(" [%s] %s via %-6s | %s\n",
status,
n.SentAt.Format("15:04:05"),
n.Channel,
truncate(n.Message, 50),
)
}
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n-3] + "..."
}
// ── Main ──────────────────────────────────────────────────────
func main() {
// Setup — dependency injection via interfaces
store := &InMemoryStore{}
svc := NewNotificationService(store)
// Register notifiers — all implement Notifier
svc.Register(&EmailNotifier{SMTPHost: "smtp.example.com", From: "[email protected]"})
svc.Register(&SlackNotifier{WebhookURL: "https://hooks.slack.com/...", Channel: "alerts"})
svc.Register(&SMSNotifier{APIKey: "sk_sms_xxx", FromNum: "+6281234567890"})
// Send various notifications
svc.Notify("[email protected]", "Welcome to our platform!", PriorityNormal)
svc.Notify("[email protected]",
"Your Rp 5,000,000 transaction was processed successfully",
PriorityHigh)
svc.Notify("[email protected]",
"CRITICAL: Server CPU usage reached 98%! Check immediately!",
PriorityCritical)
svc.Notify("[email protected]",
"Your monthly report is ready to download",
PriorityLow,
"Email") // override the channel
// View history
svc.History("[email protected]")
svc.History("[email protected]")
// Demonstrate type assertions — check for extra capabilities
fmt.Println("\n=== Notifier Info ===")
for name, notifier := range svc.notifiers {
info := fmt.Sprintf("%-10s", name)
// Type assertion to check whether it's an EmailNotifier
if emailNotifier, ok := notifier.(*EmailNotifier); ok {
info += fmt.Sprintf(" | SMTP: %s | Sent: %d",
emailNotifier.SMTPHost,
emailNotifier.sentCount)
}
// Type switch for per-type specific info
switch n := notifier.(type) {
case *SlackNotifier:
info += fmt.Sprintf(" | Channel: #%s", n.Channel)
case *SMSNotifier:
info += fmt.Sprintf(" | From: %s", n.FromNum)
}
fmt.Printf(" %s\n", info)
}
// Demonstrate: small interfaces on the consumer side
var channels []string
for name := range svc.notifiers {
channels = append(channels, name)
}
fmt.Printf("\nAvailable channels: %s\n", strings.Join(channels, ", "))
}
Summary #
- Implicit implementation — there’s no
implements; a type automatically satisfies an interface if it has all the required methods.- Interfaces = behavioral contracts — only method signatures, no fields or implementations.
- Interface values store (type, value) — an interface variable holding a nil pointer isn’t the same as a nil interface; always return
nilexplicitly.- Small interfaces are more powerful —
io.Readerwith one method is used thousands of times; avoid interfaces with 10+ methods.- Interface composition — embed interfaces into other interfaces for richer contracts.
any/interface{}loses type safety — use generics since Go 1.18 for type-safe generic containers.- Safe type assertions (
val, ok := i.(Type)) — always use the two-value form to avoid panics.- Type switches handle many possible types elegantly.
- Define interfaces on the consumer side — each consumer defines the smallest interface it needs, not one big interface on the producer side.
- Method sets:
*Thas both value and pointer receiver methods;Tonly has value receiver methods — use a pointer when assigning to an interface if pointer receivers exist.