Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact mistake a dozen times with engineers moving to Go from Java or C#. They try to treat Go slices like they treat generic lists in other languages, and they run straight into a compiler error that feels totally illogical at first glance.

package main

import "fmt"

type PaymentMethod interface {
	Pay(amount float64) string
}

type CreditCard struct {
	CardNumber string
}

func (cc CreditCard) Pay(amount float64) string {
	return fmt.Sprintf("Paid %.2f using Credit Card %s", amount, cc.CardNumber)
}

type PayPal struct {
	Email string
}

func (pp PayPal) Pay(amount float64) string {
	return fmt.Sprintf("Paid %.2f using PayPal account %s", amount, pp.Email)
}

func ProcessPayments(payments []PaymentMethod, amount float64) {
	for _, p := range payments {
		fmt.Println(p.Pay(amount))
	}
}

func main() {
	cards := []CreditCard{
		{CardNumber: "1234-5678"},
		{CardNumber: "8765-4321"},
	}

	// This is where the compiler screams at us
	ProcessPayments(cards, 100.00) 
}

The "Cannot Use Type" Slice Conflict

If you try to run this, Go will tell you: cannot use cards (variable of type []CreditCard) as type []PaymentMethod in argument to ProcessPayments.

It feels wrong, right? CreditCard implements PaymentMethod. Therefore, a slice of credit cards should be a slice of payment methods. But Go doesn't work that way. In Go, []CreditCard and []PaymentMethod have completely different memory layouts. A CreditCard struct is just the data for that struct; a PaymentMethod interface is actually a two-word data structure containing a pointer to the type information and a pointer to the actual data.

Go refuses to automatically "cast" the entire slice because it would have to allocate a brand new slice and wrap every single element in an interface header, which is a hidden performance cost. Go prefers to be explicit about those costs.

Converting Concrete Slices to Interface Slices

To fix this, you have to manually build the slice of interfaces. It's a bit tedious, but it makes the performance hit visible to you, the developer. I usually do it with a simple loop.

func main() {
	cards := []CreditCard{
		{CardNumber: "1234-5678"},
		{CardNumber: "8765-4321"},
	}

	// We create a new slice of the interface type
	paymentMethods := make([]PaymentMethod, len(cards))
	for i, v := range cards {
		paymentMethods[i] = v
	}

	ProcessPayments(paymentMethods, 100.00)
}

Implicit Satisfaction and the "Small Interface" Philosophy

The real magic of Go interfaces is that they are implicitly satisfied. Notice that CreditCard doesn't have a keyword like implements PaymentMethod. It just happens to have a Pay method with the right signature, so Go considers it a PaymentMethod. This decouples your code beautifully; you can define an interface in your package to describe what you need from a dependency, without the dependency ever knowing your interface exists.

My advice? Keep your interfaces tiny. In the Go standard library, the most powerful interfaces—like io.Reader or fmt.Stringer—often have only one single method. When you make interfaces small, they become incredibly flexible and easy to implement across different parts of your system.

Accept Interfaces, Return Structs

You'll hear this phrase a lot in the Go community. What it actually means is: don't force your functions to return an interface if they are just returning a concrete type. Return the CreditCard struct. Let the caller decide if they want to treat it as a PaymentMethod interface. This keeps your API flexible and prevents you from getting locked into a rigid hierarchy that's hard to change later.




📋 Practical Task

Build a Pluggable Notification Dispatcher

Your goal is to create a notification system that can send alerts through different channels (Email and SMS) without the main logic needing to know which channel is being used.

  • Define a Notifier interface with a method Send(message string) error.
  • Create an EmailService struct with an EmailAddress field and implement the Send method (just print "Sending Email to [address]: [message]").
  • Create an SMSService struct with a PhoneNumber field and implement the Send method (just print "Sending SMS to [number]: [message]").
  • Write a function called NotifyAll(notifiers []Notifier, msg string) that loops through a slice of notifiers and calls Send on each.
  • In your main function, initialize one of each service, put them into a slice of Notifier, and call NotifyAll.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.