Interface di Go adalah salah satu fitur paling powerful dan elegant yang membedakan Go dari bahasa pemrograman lainnya. Jika pada artikel sebelumnya kita membahas structs dan methods sebagai fondasi OOP di Go, maka interfaces adalah yang memberikan fleksibilitas dan abstraksi yang luar biasa.
“If it walks like a duck and quacks like a duck, then it must be a duck” – Duck Typing Philosophy
Inilah filosofi yang diadopsi Go dalam implementasi interface-nya, memungkinkan polymorphism yang implicit dan natural.
Apa itu Interface di Go?
Interface di Go adalah tipe data yang mendefinisikan sekumpulan method signatures (kontrak). Yang unik dari Go adalah interface implementation yang implicit – artinya, sebuah type secara otomatis mengimplementasikan interface jika memiliki semua method yang didefinisikan dalam interface tersebut.
Perbedaan dengan Bahasa Lain
// Java/C# - Explicit implementation
class Dog implements Animal {
public void makeSound() { ... }
}
// Go - Implicit implementation
type Dog struct{}
func (d Dog) MakeSound() string { return "Woof!" }
// Dog secara otomatis implement interface Animal jika ada method MakeSound()
Sintaks Dasar Interface
type InterfaceName interface {
MethodName1(parameters) returnType
MethodName2(parameters) returnType
// ... method signatures lainnya
}
Contoh Interface Sederhana
package main
import "fmt"
// Definisi interface
type Animal interface {
MakeSound() string
GetName() string
}
// Struct Dog
type Dog struct {
Name string
}
// Implementasi methods untuk Dog (implicit implementation)
func (d Dog) MakeSound() string {
return "Woof! Woof!"
}
func (d Dog) GetName() string {
return d.Name
}
// Struct Cat
type Cat struct {
Name string
}
// Implementasi methods untuk Cat
func (c Cat) MakeSound() string {
return "Meow! Meow!"
}
func (c Cat) GetName() string {
return c.Name
}
// Function yang menerima interface
func MakeAnimalSound(animal Animal) {
fmt.Printf("%s says: %s\n", animal.GetName(), animal.MakeSound())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
// Polymorphism in action
MakeAnimalSound(dog) // Buddy says: Woof! Woof!
MakeAnimalSound(cat) // Whiskers says: Meow! Meow!
// Interface slice
animals := []Animal{dog, cat}
for _, animal := range animals {
MakeAnimalSound(animal)
}
}
Interface Kosong (Empty Interface)
Interface kosong interface{} adalah interface yang tidak mendefinisikan method apapun. Karena semua type di Go memiliki zero methods (minimal), maka semua type mengimplementasikan empty interface.
package main
import "fmt"
func PrintAnything(value interface{}) {
fmt.Printf("Value: %v, Type: %T\n", value, value)
}
func main() {
PrintAnything(42)
PrintAnything("Hello World")
PrintAnything([]int{1, 2, 3})
PrintAnything(struct{ Name string }{Name: "John"})
}
Menggunakan any (Go 1.18+)
Go 1.18 memperkenalkan alias any untuk interface{} yang lebih readable:
func ProcessData(data any) {
switch v := data.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Integer:", v)
case []int:
fmt.Println("Slice of ints:", v)
default:
fmt.Println("Unknown type:", v)
}
}
Type Assertion dan Type Switch
Type Assertion
Type assertion digunakan untuk mengekstrak concrete type dari interface value:
package main
import "fmt"
func main() {
var value interface{} = "Hello, Go!"
// Type assertion dengan safety check
if str, ok := value.(string); ok {
fmt.Println("String value:", str)
} else {
fmt.Println("Not a string")
}
// Type assertion tanpa safety check (bisa panic)
str := value.(string)
fmt.Println("Direct assertion:", str)
// Ini akan panic karena value bukan int
// num := value.(int) // panic!
}
Type Switch
Type switch memungkinkan kita menangani multiple types dengan elegant:
package main
import (
"fmt"
"reflect"
)
func DescribeValue(value interface{}) {
switch v := value.(type) {
case nil:
fmt.Println("Value is nil")
case bool:
fmt.Printf("Boolean: %t\n", v)
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %q (length: %d)\n", v, len(v))
case []int:
fmt.Printf("Slice of ints: %v (count: %d)\n", v, len(v))
case map[string]int:
fmt.Printf("Map: %v (keys: %d)\n", v, len(v))
default:
fmt.Printf("Unknown type: %T, value: %v\n", v, v)
}
}
func main() {
values := []interface{}{
nil,
true,
42,
"Hello World",
[]int{1, 2, 3, 4, 5},
map[string]int{"apple": 5, "banana": 3},
struct{ Name string }{Name: "John"},
}
for _, value := range values {
DescribeValue(value)
}
}
Interface Composition
Go mendukung interface composition – menggabungkan multiple interfaces menjadi satu:
package main
import "fmt"
// Base interfaces
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
// Composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Implementation
type File struct {
name string
content []byte
pos int
closed bool
}
func (f *File) Read(data []byte) (int, error) {
if f.closed {
return 0, fmt.Errorf("file is closed")
}
n := copy(data, f.content[f.pos:])
f.pos += n
return n, nil
}
func (f *File) Write(data []byte) (int, error) {
if f.closed {
return 0, fmt.Errorf("file is closed")
}
f.content = append(f.content, data...)
return len(data), nil
}
func (f *File) Close() error {
f.closed = true
fmt.Printf("File %s closed\n", f.name)
return nil
}
func ProcessFile(rwc ReadWriteCloser) {
// Write some data
rwc.Write([]byte("Hello, World!"))
// Read the data
buffer := make([]byte, 13)
n, _ := rwc.Read(buffer)
fmt.Printf("Read %d bytes: %s\n", n, string(buffer[:n]))
// Close the file
rwc.Close()
}
func main() {
file := &File{
name: "example.txt",
content: make([]byte, 0),
pos: 0,
closed: false,
}
ProcessFile(file)
}
Standard Library Interfaces
Go standard library menggunakan interfaces secara ekstensif. Memahami interface standar akan membantu Anda menulis code yang lebih idiomatis.
io.Reader dan io.Writer
package main
import (
"fmt"
"io"
"strings"
)
// Custom reader
type NumberReader struct {
numbers []int
index int
}
func (nr *NumberReader) Read(p []byte) (n int, err error) {
if nr.index >= len(nr.numbers) {
return 0, io.EOF
}
data := fmt.Sprintf("%d\n", nr.numbers[nr.index])
nr.index++
n = copy(p, []byte(data))
return n, nil
}
func ProcessReader(r io.Reader) {
buffer := make([]byte, 1024)
for {
n, err := r.Read(buffer)
if err == io.EOF {
break
}
if err != nil {
fmt.Printf("Error: %v\n", err)
break
}
fmt.Print(string(buffer[:n]))
}
}
func main() {
// String reader
stringReader := strings.NewReader("Hello from string reader!\n")
fmt.Println("=== String Reader ===")
ProcessReader(stringReader)
// Custom number reader
numberReader := &NumberReader{
numbers: []int{1, 2, 3, 4, 5},
index: 0,
}
fmt.Println("\n=== Number Reader ===")
ProcessReader(numberReader)
}
fmt.Stringer Interface
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
// Implement fmt.Stringer interface
func (p Person) String() string {
return fmt.Sprintf("%s %s (%d years old)", p.FirstName, p.LastName, p.Age)
}
type Product struct {
Name string
Price float64
}
func (p Product) String() string {
return fmt.Sprintf("%s - $%.2f", p.Name, p.Price)
}
func main() {
person := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
product := Product{
Name: "Laptop",
Price: 999.99,
}
// fmt.Println automatically calls String() method
fmt.Println(person)
fmt.Println(product)
// Can also be used in format strings
fmt.Printf("Person info: %s\n", person)
fmt.Printf("Product info: %v\n", product)
}
Interface dengan Error Handling
Interface error adalah salah satu interface paling penting di Go:
type error interface {
Error() string
}
Custom Error Types
package main
import (
"fmt"
"time"
)
// Custom error type
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (ve ValidationError) Error() string {
return fmt.Sprintf("validation failed for field '%s' with value '%v': %s",
ve.Field, ve.Value, ve.Message)
}
// Another custom error type
type TimeoutError struct {
Operation string
Duration time.Duration
}
func (te TimeoutError) Error() string {
return fmt.Sprintf("operation '%s' timed out after %v", te.Operation, te.Duration)
}
// Function that returns custom errors
func ValidateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Value: age,
Message: "age cannot be negative",
}
}
if age > 150 {
return ValidationError{
Field: "age",
Value: age,
Message: "age seems unrealistic",
}
}
return nil
}
func ProcessWithTimeout(operation string, duration time.Duration) error {
if duration > 5*time.Second {
return TimeoutError{
Operation: operation,
Duration: duration,
}
}
return nil
}
func HandleError(err error) {
if err == nil {
return
}
// Type assertion untuk custom error handling
switch e := err.(type) {
case ValidationError:
fmt.Printf("Validation Error - Field: %s, Value: %v\n", e.Field, e.Value)
case TimeoutError:
fmt.Printf("Timeout Error - Operation: %s took too long\n", e.Operation)
default:
fmt.Printf("Unknown Error: %s\n", err.Error())
}
}
func main() {
// Test validation errors
errors := []error{
ValidateAge(-5),
ValidateAge(200),
ValidateAge(25),
ProcessWithTimeout("database_query", 10*time.Second),
ProcessWithTimeout("api_call", 2*time.Second),
}
for _, err := range errors {
HandleError(err)
}
}
Practical Example: Plugin System dengan Interfaces
Mari kita buat contoh sistem plugin yang mendemonstrasikan kekuatan interfaces:
package main
import (
"encoding/json"
"fmt"
"strings"
)
// Plugin interface
type DataProcessor interface {
Name() string
Process(data []byte) ([]byte, error)
Description() string
}
// JSON Formatter Plugin
type JSONFormatter struct{}
func (jf JSONFormatter) Name() string {
return "json_formatter"
}
func (jf JSONFormatter) Description() string {
return "Formats data as pretty JSON"
}
func (jf JSONFormatter) Process(data []byte) ([]byte, error) {
var obj interface{}
if err := json.Unmarshal(data, &obj); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
formatted, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to format JSON: %w", err)
}
return formatted, nil
}
// Text Uppercase Plugin
type TextUppercase struct{}
func (tu TextUppercase) Name() string {
return "text_uppercase"
}
func (tu TextUppercase) Description() string {
return "Converts text to uppercase"
}
func (tu TextUppercase) Process(data []byte) ([]byte, error) {
return []byte(strings.ToUpper(string(data))), nil
}
// Word Count Plugin
type WordCounter struct{}
func (wc WordCounter) Name() string {
return "word_counter"
}
func (wc WordCounter) Description() string {
return "Counts words in text"
}
func (wc WordCounter) Process(data []byte) ([]byte, error) {
text := string(data)
words := strings.Fields(text)
result := fmt.Sprintf("Word count: %d\nCharacter count: %d\nLine count: %d",
len(words), len(text), strings.Count(text, "\n")+1)
return []byte(result), nil
}
// Plugin Manager
type PluginManager struct {
plugins []DataProcessor
}
func NewPluginManager() *PluginManager {
return &PluginManager{
plugins: make([]DataProcessor, 0),
}
}
func (pm *PluginManager) RegisterPlugin(plugin DataProcessor) {
pm.plugins = append(pm.plugins, plugin)
fmt.Printf("Plugin registered: %s - %s\n", plugin.Name(), plugin.Description())
}
func (pm *PluginManager) ListPlugins() {
fmt.Println("\nAvailable Plugins:")
for i, plugin := range pm.plugins {
fmt.Printf("%d. %s - %s\n", i+1, plugin.Name(), plugin.Description())
}
}
func (pm *PluginManager) ProcessData(pluginName string, data []byte) ([]byte, error) {
for _, plugin := range pm.plugins {
if plugin.Name() == pluginName {
return plugin.Process(data)
}
}
return nil, fmt.Errorf("plugin '%s' not found", pluginName)
}
func (pm *PluginManager) ProcessWithAllPlugins(data []byte) {
fmt.Println("\nProcessing with all plugins:")
for _, plugin := range pm.plugins {
fmt.Printf("\n--- %s ---\n", plugin.Name())
result, err := plugin.Process(data)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("%s\n", string(result))
}
}
func main() {
// Create plugin manager
pm := NewPluginManager()
// Register plugins
pm.RegisterPlugin(JSONFormatter{})
pm.RegisterPlugin(TextUppercase{})
pm.RegisterPlugin(WordCounter{})
// List available plugins
pm.ListPlugins()
// Sample data
jsonData := `{"name": "John Doe", "age": 30, "city": "New York"}`
textData := "Hello World! This is a sample text for processing."
fmt.Println("\nProcessing JSON data:")
result, err := pm.ProcessData("json_formatter", []byte(jsonData))
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("%s\n", string(result))
}
fmt.Println("\nProcessing text data with all plugins:")
pm.ProcessWithAllPlugins([]byte(textData))
}
Interface Best Practices
1. Keep Interfaces Small
// Good: Small, focused interfaces
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// Avoid: Large interfaces with many methods
type BadFileManager interface {
Open(string) error
Close() error
Read([]byte) (int, error)
Write([]byte) (int, error)
Seek(int64, int) (int64, error)
Stat() (FileInfo, error)
Chmod(FileMode) error
// ... many more methods
}
2. Accept Interfaces, Return Concrete Types
// Good: Accept interface
func ProcessData(r io.Reader) []byte {
// implementation
return nil
}
// Good: Return concrete type
func NewFileReader(filename string) *FileReader {
return &FileReader{filename: filename}
}
// Avoid: Return interface (unless necessary)
func NewReader(filename string) io.Reader {
return &FileReader{filename: filename}
}
3. Interface Naming Conventions
// Good: Single method interfaces often end with -er
type Runner interface {
Run() error
}
type Validator interface {
Validate() error
}
type Serializer interface {
Serialize() ([]byte, error)
}
4. Use Empty Interface Sparingly
// Good: Specific interface
func ProcessUser(u User) error {
// type-safe operations
return nil
}
// Use with caution: Empty interface
func ProcessAnyData(data interface{}) error {
// requires type assertions
return nil
}
Interface vs Struct: Kapan Menggunakan Yang Mana?
Gunakan Interface Ketika:
- Anda perlu abstraksi untuk multiple implementations
- Ingin membuat code yang testable (easy mocking)
- Perlu polymorphism
- Ingin loose coupling antar components
Gunakan Struct Ketika:
- Anda memiliki concrete data yang perlu diorganisir
- Tidak perlu abstraksi
- Single implementation sudah cukup
Testing dengan Interfaces
Interface memudahkan unit testing dengan mocking:
package main
import (
"errors"
"fmt"
"testing"
)
// Service interface
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(name, email string) (*User, error)
}
type User struct {
ID int
Name string
Email string
}
// Real implementation
type DatabaseUserService struct {
// database connection, etc.
}
func (dus *DatabaseUserService) GetUser(id int) (*User, error) {
// real database query
return &User{ID: id, Name: "John Doe", Email: "john@example.com"}, nil
}
func (dus *DatabaseUserService) CreateUser(name, email string) (*User, error) {
// real database insert
return &User{ID: 123, Name: name, Email: email}, nil
}
// Mock implementation for testing
type MockUserService struct {
users map[int]*User
lastID int
}
func NewMockUserService() *MockUserService {
return &MockUserService{
users: make(map[int]*User),
lastID: 0,
}
}
func (mus *MockUserService) GetUser(id int) (*User, error) {
if user, exists := mus.users[id]; exists {
return user, nil
}
return nil, errors.New("user not found")
}
func (mus *MockUserService) CreateUser(name, email string) (*User, error) {
mus.lastID++
user := &User{
ID: mus.lastID,
Name: name,
Email: email,
}
mus.users[mus.lastID] = user
return user, nil
}
// Business logic that depends on UserService
type UserController struct {
userService UserService
}
func NewUserController(us UserService) *UserController {
return &UserController{userService: us}
}
func (uc *UserController) RegisterUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
if email == "" {
return nil, errors.New("email is required")
}
return uc.userService.CreateUser(name, email)
}
// Test function (would be in *_test.go file)
func TestUserController_RegisterUser(t *testing.T) {
// Setup mock
mockService := NewMockUserService()
controller := NewUserController(mockService)
// Test successful registration
user, err := controller.RegisterUser("Alice", "alice@example.com")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if user.Name != "Alice" {
t.Errorf("Expected name 'Alice', got %s", user.Name)
}
// Test validation
_, err = controller.RegisterUser("", "test@example.com")
if err == nil {
t.Error("Expected error for empty name")
}
}
func main() {
fmt.Println("Running interface testing example...")
// This would typically be in a test file
t := &testing.T{}
TestUserController_RegisterUser(t)
fmt.Println("Test completed!")
}
Kesimpulan
Interface di Go adalah salah satu fitur paling powerful yang memungkinkan:
- Duck Typing – Implicit implementation yang flexible
- Polymorphism – Multiple types dengan behavior yang sama
- Abstraksi – Memisahkan contract dari implementation
- Testability – Easy mocking untuk unit tests
- Composition – Menggabungkan interfaces untuk functionality yang complex
- Loose Coupling – Mengurangi dependency antar components
Key Points untuk diingat:
- Interface implementation di Go adalah implicit
- Empty interface dapat menyimpan value type apapun
- Type assertion dan type switch untuk mengakses concrete types
- Interface composition memungkinkan building complex contracts
- Small interfaces lebih baik daripada large interfaces
- Accept interfaces, return concrete types adalah best practice
Interface di Go membuka pintu untuk design patterns yang elegant dan maintainable code architecture. Dengan memahami konsep duck typing dan implicit implementation, Anda dapat menulis Go code yang lebih fleksibel dan robust.
Di artikel selanjutnya, kita akan membahas Error Handling di Go yang akan menunjukkan bagaimana Go menangani error dengan pendekatan yang unique dan explicit.