Interfaces are Go’s most powerful feature. Unlike Java or C#, Go interfaces are satisfied implicitly—you never say implements Notifier. If a type has the required methods, it automatically satisfies the interface.
“Accept interfaces, return structs” - This is the golden rule of Go API design.
Dependency Injection means passing dependencies as parameters instead of creating them inside functions.
task2/main.go
package mainimport "fmt"type Notifier interface { Notify() string}type Email struct { address string}func (e Email) Notify() string { return "Email sent to " + e.address}// The function says: "I don't know what you are,// but I know you can Notify"func send(n Notifier) { fmt.Println(n.Notify())}func main() { email := Email{address: "user@example.com"} send(email)}
This pattern is the foundation for writing maintainable Go code:
task5/main.go
package mainimport "fmt"/*STEP 1: Define the behavior you care about.This is the interface.*/type Notifier interface { Notify() string}/*STEP 2: Create a concrete implementation (Email).*/type Email struct { address string}func (e Email) Notify() string { return "Email sent to " + e.address}/*STEP 3: Create another implementation (SMS).*/type SMS struct { number string}func (s SMS) Notify() string { return "SMS sent to " + s.number}/*STEP 4: Business logic that depends on the INTERFACE,not on Email or SMS.*/func sendNotification(n Notifier) { // This function has NO IDEA what concrete type n is. // It only knows one thing: n can Notify(). fmt.Println(n.Notify())}func main() { email := Email{address: "user@example.com"} sms := SMS{number: "9999999999"} sendNotification(email) sendNotification(sms)}
1
Step 1: Define Behavior
Create an interface that describes what you need, not how it’s done.
2
Step 2: First Implementation
Build a concrete type that satisfies the interface.
3
Step 3: Second Implementation
Add another type. This proves your abstraction is useful.
4
Step 4: Depend on the Interface
Write business logic that depends on the interface, not concrete types.
Don’t create interfaces before you need them. Wait until you have at least 2 implementations before abstracting.
var i interface{} = "hello"// Type assertions := i.(string)fmt.Println(s) // "hello"// Safe type assertions, ok := i.(string)if ok { fmt.Println(s)}// Panics if wrong typen := i.(int) // ❌ panic: interface conversion
Handle multiple types:
func describe(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Integer: %d\n", v) case string: fmt.Printf("String: %s\n", v) case bool: fmt.Printf("Boolean: %v\n", v) default: fmt.Printf("Unknown type: %T\n", v) }}describe(42) // Integer: 42describe("hello") // String: hellodescribe(true) // Boolean: true