Kami berurusan dengan antarmuka di Go


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:

  1. Untuk menjelaskan dalam bahasa manusia apa antarmuka itu.
  2. Jelaskan bagaimana mereka berguna dan bagaimana Anda dapat menggunakannya dalam kode Anda.
  3. Bicara tentang apa interface{} (antarmuka kosong).
  4. 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" ) //   Book,    fmt.Stringer. type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } //   Count,    fmt.Stringer. type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } //   WriteLog(),    , //   fmt.Stringer   . func WriteLog(s fmt.Stringer) { log.Println(s.String()) } func main() { //   Book    WriteLog(). book := Book{"Alice in Wonderland", "Lewis Carrol"} WriteLog(book) //   Count    WriteLog(). count := Count(3) WriteLog(count) } 

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:

  1. Antarmuka membantu mengurangi duplikasi, yaitu jumlah kode boilerplate.
  2. Mereka membuatnya lebih mudah untuk menggunakan bertopik pada unit test daripada benda nyata.
  3. 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" ) //   Customer. type Customer struct { Name string Age int } //   WriteJSON,   io.Writer   . //    ustomer  JSON,     // ,     Write()  io.Writer. func (c *Customer) WriteJSON(w io.Writer) error { js, err := json.Marshal(c) if err != nil { return err } _, err = w.Write(js) return err } func main() { //   Customer. c := &Customer{Name: "Alice", Age: 21} //    Buffer    WriteJSON var buf bytes.Buffer err := c.WriteJSON(buf) if err != nil { log.Fatal(err) } //   . f, err := os.Create("/tmp/customer") if err != nil { log.Fatal(err) } defer f.Close() err = c.WriteJSON(f) if err != nil { log.Fatal(err) } } 

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:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr, err := calculateSalesRate(shopDB) if err != nil { log.Fatal(err) } fmt.Printf(sr) } func calculateSalesRate(sdb *ShopDB) (string, error) { since := time.Now().Sub(24 * time.Hour) sales, err := sdb.CountSales(since) if err != nil { return "", err } customers, err := sdb.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

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:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) //    ShopModel.     //     ,     //  -,     . type ShopModel interface { CountCustomers(time.Time) (int, error) CountSales(time.Time) (int, error) } //  ShopDB    ShopModel,   //       -- CountCustomers()  CountSales(). type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr := calculateSalesRate(shopDB) fmt.Printf(sr) } //       ShopModel    //    *ShopDB. func calculateSalesRate(sm ShopModel) string { since := time.Now().Sub(24 * time.Hour) sales, err := sm.CountSales(since) if err != nil { return "", err } customers, err := sm.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

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:

 // : main_test.go package main import ( "testing" ) type MockShopDB struct{} func (m *MockShopDB) CountCustomers() (int, error) { return 1000, nil } func (m *MockShopDB) CountSales() (int, error) { return 333, nil } func TestCalculateSalesRate(t *testing.T) { //  . m := &MockShopDB{} //     calculateSalesRate(). sr := calculateSalesRate(m) // ,        //   . exp := "0.33" if sr != exp { t.Fatalf("got %v; expected %v", sr, exp) } } 

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 .

Source: https://habr.com/ru/post/id463063/


All Articles