---
id: ebf46089667a6469b258b8e52e57571f
author: Yunjin Lee
title: 'Antarmuka dan embedding struktur dalam Go: perbedaannya dengan pewarisan kelas dan desain komposisi'
description: Artikel ini membandingkan perbedaan antara pewarisan kelas pada Java/C++ dan structural typing pada interface Go, serta menjelaskan pola komposisi (Composition) dengan memanfaatkan struct embedding dan delegasi method melalui contoh kode.
language: id
date: 2025-07-17T10:40:02.284435348Z
path: /blog/posts/go-interfaces-are-not-inheritance-zd04c1084
category: language
---

# Ikhtisar

Antarmuka (interface) di Go memudahkan sebuah fungsi dengan argumen dan nilai kembalian yang sama untuk dimiliki oleh banyak struct, tetapi cara ini berbeda dengan cara kata kunci extends di Java yang memperluas serta menimpa (override) perilaku fungsi di dalamnya secara tepat. Pemahaman yang benar tentang penggunaan ulang kode komposisional di Go diperlukan agar tidak tertukar dengan pewarisan (inheritance), tetapi memahaminya secara teoretis dan sempurna sejak awal memang sulit. Mari kita pelajari bersama dengan skenario yang mudah menimbulkan kesalahan.

## Kesalahan yang Sering Dilakukan

Pemula mungkin melakukan kesalahan-kesalahan berikut.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type Apple struct {
	Label string
	Name  string
	Brix  float64
}

type Watermelon struct {
	Label string
	Name  string
	Brix  float64
}

func (a *Apple) PrintAll() {
	fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
}

const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
}

func main() {
	fmt.Println("Inheritance test #1")
	apple := new(Apple)
	watermelon := apple
	apple.SetLabel("Apple_1")
	fmt.Println("Apple, before copied to Watermelon")
	apple.PrintAll()
	watermelon.SetLabel("WaterMelon_2")
	fmt.Println("Apple, after copied to Watermelon")
	apple.PrintAll()
	fmt.Println("Watermelon, which inherited Apple's Method")
	watermelon.PrintAll()
}
```

Kode seperti ini tampak tidak bermasalah jika kita keliru menganggap bahwa Go mengikuti pewarisan tradisional. Namun, keluarannya adalah sebagai berikut.

```
Inheritance test #1
Succeed: Label was  Apple_1
Apple, before copied to Watermelon
Fruit: apple, Label: Apple_1, Brix: 14.5
Failed: Label was  WaterMelon_2
Apple, after copied to Watermelon
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
Watermelon, which inherited Apple's Method
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
```

Di sini perilaku Go menjadi jelas hanya melalui baris berikut.

```go
watermelon := apple
```

Kode ini sama sekali tidak mengubah Apple menjadi kelas Watermelon secara langsung.
Melainkan, watermelon hanyalah pointer yang menunjuk ke apple.

Sekali lagi ditegaskan di sini, **Go tidak mengikuti konsep pewarisan tradisional.**

Jika kode ditulis dengan kesalahpahaman seperti ini, akan muncul kesalahan fatal seperti pembuatan pointer yang tidak berarti dan penyalinan fungsi untuk struct lain yang tidak terduga.

Lalu, seperti apakah kode yang ideal?

## Contoh yang Tepat dalam Bahasa Go

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	Fruit BaseFruit
}

type Watermelon struct {
	Label string
	Fruit BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Fruit.Brix 	= 14.5;
	a.Fruit.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Fruit.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
	a.Fruit.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Fruit.Brix = 10;
	w.Fruit.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Fruit.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
	w.Fruit.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

Namun, di Go tetap dimungkinkan untuk membuat sesuatu tampak **seperti** pewarisan.
Contohnya adalah embedding anonim. Hal ini dapat dilakukan dengan mendeklarasikan struct internal sebagai struct tanpa nama.
Dalam kasus ini, **field dari struct bawahan dapat diakses secara langsung** tanpa perlu menyebutkannya secara eksplisit.
Dengan menggunakan pola **promosi** field struct bawahan ke struct atas, keterbacaan kode dapat meningkat dalam kondisi tertentu. Akan tetapi, jika struct bawahan perlu ditampilkan secara eksplisit, pola ini tidak disarankan.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	BaseFruit
}

type Watermelon struct {
	Label string
	BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
	a.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
	w.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

Pada contoh ini terdapat perbedaan sebagai berikut.

```go
w.PrintAll() // Pemanggilan otomatis melalui promosi struct tanpa nama, bukan w.Friut.PrintAll()
```

Kedua contoh memiliki poin penting sebagai berikut.

- main dibuat sederhana, fungsi dikelompokkan menurut fungsinya
- untuk struct yang berbeda, gunakan objek yang berbeda
- jika diperlukan berbagi, gunakan struct internal

Apa keuntungan dari filosofi pemrograman seperti ini?

## Keuntungan

- Pemisahan yang jelas antara method yang perlu dibagikan dan yang tidak
- Pemisahan tanggung jawab pada masing-masing struct dan method
- Kode yang terpisah secara struktural sesuai spesifikasi fungsi yang dibutuhkan

Pada awalnya bahasa Go mungkin terasa asing karena berbeda dari OOP tradisional, tetapi setelah terbiasa, pemrograman yang eksplisit menjadi mungkin.

## Ringkasan

- Isolasikan tanggung jawab
- Pisahkan secara terperinci per unit struct
- Method tidak boleh dipahami seperti abstract class di Java
- Lakukan pemrograman yang eksplisit dan konkret
  Bahasa Go harus diperlakukan secara lebih sederhana, jelas, dan terpisah dibandingkan model OOP tradisional. Alih-alih memprogram secara ekstensif, mari tulis kode dengan memisahkannya secara bertahap dan terstruktur.
