Dalam beberapa bulan terakhir, saya telah melakukan
penelitian yang menanyakan kepada orang-orang apa yang sulit untuk mereka pahami di Go. Dan saya perhatikan bahwa konsep antarmuka secara teratur disebutkan dalam jawaban. Go adalah bahasa antarmuka pertama yang saya gunakan, dan saya ingat bahwa pada saat itu konsep ini tampak sangat membingungkan. Dan dalam panduan ini, saya ingin melakukan ini:
- Untuk menjelaskan dalam bahasa manusia apa antarmuka itu.
- Jelaskan bagaimana mereka berguna dan bagaimana Anda dapat menggunakannya dalam kode Anda.
- Bicara tentang apa
interface{}
(antarmuka kosong). - Dan berjalanlah melalui beberapa jenis antarmuka berguna yang dapat Anda temukan di perpustakaan standar.
Jadi apa itu antarmuka?
Jenis antarmuka di Go adalah semacam
definisi . Ini mendefinisikan dan menjelaskan metode spesifik yang
harus dimiliki beberapa tipe lain .
Salah satu jenis antarmuka dari perpustakaan standar adalah antarmuka
fmt.Stringer :
type Stringer interface { String() string }
Kami mengatakan bahwa sesuatu
memenuhi antarmuka ini (atau
mengimplementasikan antarmuka ini ) jika "sesuatu" ini memiliki metode dengan nilai string tanda tangan khusus
String()
.
Misalnya, tipe
Book
memenuhi antarmuka karena memiliki metode
String()
:
type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) }
Tidak masalah apa jenis
Book
atau apa fungsinya. Yang penting adalah ia memiliki metode yang disebut
String()
yang mengembalikan nilai string.
Ini adalah contoh lain. Tipe
Count
juga
memenuhi antarmuka fmt.Stringer
karena memiliki metode dengan nilai string tanda tangan yang sama
String()
.
type Count int func (c Count) String() string { return strconv.Itoa(int(c)) }
Penting untuk dipahami di sini bahwa kita memiliki dua jenis
Book
dan
Count
yang berbeda, yang bertindak secara berbeda. Tetapi mereka disatukan oleh fakta bahwa mereka berdua memenuhi antarmuka
fmt.Stringer
.
Anda bisa melihatnya dari sisi lain. Jika Anda tahu bahwa objek memenuhi antarmuka
fmt.Stringer
, maka Anda dapat mengasumsikan bahwa ia memiliki metode dengan nilai string tanda tangan
String()
yang dapat Anda panggil.
Dan sekarang yang paling penting.
Saat Anda melihat deklarasi di Go (dari variabel, parameter fungsi, atau bidang struktur) yang memiliki tipe antarmuka, Anda bisa menggunakan objek jenis apa saja selama memenuhi antarmuka.Katakanlah kita memiliki fungsi:
func WriteLog(s fmt.Stringer) { log.Println(s.String()) }
Karena
WriteLog()
menggunakan tipe antarmuka
fmt.Stringer
dalam
fmt.Stringer
parameter, kita dapat melewatkan objek apa pun yang memenuhi antarmuka
fmt.Stringer
. Misalnya, kita bisa meneruskan jenis
Book
dan
Count
yang kita buat sebelumnya dalam metode
WriteLog()
, dan kode akan berfungsi dengan baik.
Selain itu, karena objek yang dikirimkan memenuhi antarmuka
fmt.Stringer
, kita
tahu bahwa ia memiliki metode
String()
, yang dapat dengan aman dipanggil oleh fungsi
WriteLog()
.
Mari kita satukan semuanya dalam satu contoh, menunjukkan kekuatan antarmuka.
package main import ( "fmt" "strconv" "log" )
Ini luar biasa. Dalam fungsi utama, kami membuat berbagai jenis
Book
dan
Count
, tetapi meneruskannya ke fungsi
WriteLog()
sama . Dan dia memanggil fungsi
String()
sesuai dan menulis hasilnya ke log.
Jika Anda
mengeksekusi kode , Anda akan mendapatkan hasil yang serupa:
2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3
Kami tidak akan membahas hal ini secara rinci. Hal utama yang perlu diingat: menggunakan tipe antarmuka dalam deklarasi fungsi
WriteLog()
, kami membuat fungsi acuh tak acuh (atau fleksibel) dengan
jenis objek yang diterima. Yang penting adalah
metode apa yang dia miliki .
Apa antarmuka yang bermanfaat?
Ada sejumlah alasan mengapa Anda bisa mulai menggunakan antarmuka di Go. Dan dalam pengalaman saya, yang paling penting adalah:
- Antarmuka membantu mengurangi duplikasi, yaitu jumlah kode boilerplate.
- Mereka membuatnya lebih mudah untuk menggunakan bertopik pada unit test daripada benda nyata.
- Menjadi alat arsitektur, antarmuka membantu membuka bagian-bagian basis kode Anda.
Mari kita lihat lebih dekat cara-cara ini menggunakan antarmuka.
Kurangi jumlah kode boilerplate
Misalkan kita memiliki struktur
Customer
yang berisi beberapa jenis data pelanggan. Di satu bagian dari kode, kami ingin menulis informasi ini ke
bytes.Buffer , dan di bagian lain kami ingin menulis data klien ke
os.File pada disk. Namun, dalam kedua kasus tersebut, kami ingin membuat serialisasi struktur
ustomer
menjadi JSON.
Dalam skenario ini, kita bisa mengurangi jumlah kode boilerplate menggunakan antarmuka Go.
Go memiliki jenis antarmuka
io.Writer :
type Writer interface { Write(p []byte) (n int, err error) }
Dan kita dapat mengambil keuntungan dari fakta bahwa
bytes.Buffer dan tipe
os.File memenuhi antarmuka ini, karena mereka memiliki metode
bytes.Buffer.Write () dan
os.File.Write () , masing-masing.
Implementasi sederhana:
package main import ( "encoding/json" "io" "log" "os" )
Tentu saja, ini hanya contoh fiktif (kita dapat menyusun kode secara berbeda untuk mencapai hasil yang sama). Tapi itu menggambarkan dengan baik keuntungan menggunakan antarmuka: kita dapat membuat metode
Customer.WriteJSON()
sekali dan menyebutnya setiap kali kita perlu menulis sesuatu yang memenuhi antarmuka
io.Writer
.
Tetapi jika Anda baru menggunakan Go, Anda akan memiliki beberapa pertanyaan: โ
Bagaimana saya tahu jika antarmuka io.Writer ada? Dan bagaimana Anda tahu sebelumnya bahwa dia puas dengan bytes.Buffer
dan os.File
? "
Saya khawatir tidak ada solusi sederhana. Anda hanya perlu mendapatkan pengalaman, berkenalan dengan antarmuka dan berbagai jenis dari perpustakaan standar. Ini akan membantu membaca dokumentasi untuk perpustakaan ini dan melihat kode orang lain. Dan untuk referensi cepat, saya menambahkan jenis-jenis antarmuka yang paling berguna pada akhir artikel.
Tetapi bahkan jika Anda tidak menggunakan antarmuka dari perpustakaan standar, tidak ada yang mencegah Anda membuat dan menggunakan
jenis antarmuka Anda sendiri . Kami akan membicarakan ini di bawah ini.
Pengujian dan Rintisan Unit
Untuk memahami bagaimana antarmuka membantu dalam pengujian unit, mari kita lihat contoh yang lebih kompleks.
Misalkan Anda memiliki informasi toko dan toko tentang penjualan dan jumlah pelanggan di PostgreSQL. Anda ingin menulis kode yang menghitung pangsa penjualan (jumlah penjualan spesifik per pelanggan) untuk hari terakhir, dibulatkan menjadi dua tempat desimal.
Implementasi minimal akan terlihat seperti ini:
Sekarang kita ingin membuat unit test untuk fungsi
calculateSalesRate()
untuk memverifikasi bahwa perhitungannya benar.
Sekarang bermasalah. Kita perlu mengonfigurasi contoh pengujian PostgreSQL, serta membuat dan menghapus skrip untuk mengisi basis data dengan data palsu. Kami memiliki banyak pekerjaan yang harus dilakukan jika kami benar-benar ingin menguji perhitungan kami.
Dan antarmuka datang untuk menyelamatkan!
Kami akan membuat jenis antarmuka kami sendiri yang menggambarkan metode
CountSales()
dan
CountCustomers()
, yang bergantung pada fungsi
CountCustomers()
. Kemudian perbarui tanda tangan
*ShopDB
calculateSalesRate()
untuk menggunakan tipe antarmuka ini sebagai parameter alih-alih tipe
*ShopDB
ditentukan.
Seperti ini:
Setelah kami melakukan ini, kami hanya akan membuat tulisan rintisan yang memenuhi antarmuka
ShopModel
. Kemudian Anda dapat menggunakannya selama pengujian unit operasi logika matematika yang benar dalam fungsi
calculateSalesRate()
. Seperti ini:
Sekarang jalankan tes dan semuanya berfungsi dengan baik.
Arsitektur aplikasi
Pada contoh sebelumnya, kami melihat bagaimana Anda dapat menggunakan antarmuka untuk memisahkan bagian-bagian tertentu dari kode dari menggunakan jenis tertentu. Misalnya, fungsi
ShopModel
calculateSalesRate()
tidak masalah apa yang Anda berikan, asalkan memenuhi antarmuka
ShopModel
.
Anda dapat memperluas ide ini dan membuat seluruh level "tidak terikat" dalam proyek-proyek besar.
Misalkan Anda membuat aplikasi web yang berinteraksi dengan database. Jika Anda membuat antarmuka yang menjelaskan metode tertentu untuk berinteraksi dengan database, Anda bisa merujuknya daripada jenis tertentu melalui penangan HTTP. Karena penangan HTTP hanya merujuk ke antarmuka, ini akan membantu memisahkan tingkat HTTP dan tingkat interaksi dengan basis data satu sama lain. Akan lebih mudah untuk bekerja dengan level secara mandiri, dan di masa depan Anda akan dapat mengganti beberapa level tanpa memengaruhi pekerjaan orang lain.
Saya menulis tentang pola ini di
salah satu posting sebelumnya , ada lebih banyak detail dan contoh praktis.
Apa itu antarmuka kosong?
Jika Anda telah pemrograman di Go untuk beberapa waktu, maka Anda mungkin menemukan
antarmuka tipe antarmuka kosong interface{}
. Saya akan mencoba menjelaskan apa itu. Di awal artikel ini, saya menulis:
Jenis antarmuka di Go adalah semacam definisi . Ini mendefinisikan dan menjelaskan metode spesifik yang harus dimiliki beberapa tipe lain .
Jenis antarmuka kosong
tidak menjelaskan metode . Dia tidak punya aturan. Dan objek apa pun memenuhi antarmuka kosong.
Intinya, antarmuka jenis antarmuka kosong
interface{}
adalah sejenis joker. Jika Anda bertemu dalam deklarasi (variabel, parameter fungsi atau bidang struktur), maka Anda dapat menggunakan objek
jenis apa pun .
Pertimbangkan kodenya:
package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) }
Di sini kita menginisialisasi peta menjadi
person
, yang menggunakan tipe string untuk kunci dan antarmuka tipe antarmuka kosong
interface{}
untuk nilai. Kami menetapkan tiga jenis berbeda sebagai nilai peta (string, integer dan float32), dan tidak ada masalah. Karena objek jenis apa pun memenuhi antarmuka kosong, kodenya berfungsi dengan baik.
Anda dapat
menjalankan kode ini di sini , Anda akan melihat hasil yang serupa:
map[age:21 height:167.64 name:Alice]
Ketika datang untuk mengekstraksi dan menggunakan nilai-nilai dari peta, penting untuk diingat ini. Misalkan Anda ingin mendapatkan nilai
age
dan meningkatkannya dengan 1. Jika Anda menulis kode yang sama, maka itu tidak akan dikompilasi:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) }
Anda akan menerima pesan kesalahan:
invalid operation: person["age"] + 1 (mismatched types interface {} and int)
Alasannya adalah bahwa nilai yang disimpan dalam peta mengambil tipe
interface{}
dan kehilangan tipe int dasarnya. Dan karena nilainya tidak lagi bilangan bulat, kami tidak dapat menambahkan 1 ke dalamnya.
Untuk menyiasatinya, Anda perlu membuat integer nilai lagi, dan baru menggunakannya:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) }
Jika Anda
menjalankan ini , semuanya akan berfungsi seperti yang diharapkan:
2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice]
Jadi kapan Anda harus menggunakan jenis antarmuka kosong?
Mungkin
tidak terlalu sering . Jika Anda sampai pada ini, maka berhentilah dan pikirkan apakah benar menggunakan
interface{}
. Sebagai saran umum, saya dapat mengatakan bahwa akan lebih jelas, lebih aman dan lebih produktif untuk menggunakan tipe tertentu, yaitu tipe antarmuka yang tidak kosong. Dalam contoh di atas, lebih baik untuk mendefinisikan struktur
Person
dengan bidang yang diketik dengan tepat:
type Person struct { Name string Age int Height float32 }
Antarmuka kosong, di sisi lain, berguna ketika Anda perlu mengakses dan bekerja dengan jenis yang tidak dapat diprediksi atau ditentukan oleh pengguna. Untuk beberapa alasan, antarmuka tersebut digunakan di tempat yang berbeda di perpustakaan standar, misalnya, di
gob.Encode ,
fmt.Print, dan fungsi
template.Execute .
Jenis Antarmuka yang Berguna
Berikut adalah daftar pendek jenis antarmuka yang paling banyak diminta dan berguna dari perpustakaan standar. Jika Anda belum terbiasa dengan mereka, maka saya sarankan membaca dokumentasi yang relevan.
Daftar pustaka standar yang lebih panjang juga tersedia di
sini .