Kerangka API Golang

Dalam proses untuk mengenal Golang, saya memutuskan untuk membuat kerangka aplikasi, yang akan nyaman bagi saya untuk bekerja dengan di masa depan. Hasilnya adalah, menurut pendapat saya, benda kerja yang baik, yang saya putuskan untuk dibagikan, dan pada saat yang sama membahas momen-momen yang muncul selama pembuatan bingkai.


gambar


Pada prinsipnya, desain bahasa Go mengisyaratkan bahwa ia tidak perlu membuat aplikasi skala besar (saya berbicara tentang kurangnya obat generik dan mekanisme penanganan kesalahan yang tidak terlalu kuat). Tetapi kita masih tahu bahwa ukuran aplikasi biasanya tidak berkurang, tetapi lebih sering justru sebaliknya. Oleh karena itu, lebih baik untuk segera membuat kerangka kerja yang memungkinkan untuk merangkai fungsi baru tanpa mengorbankan dukungan kode.


Saya mencoba memasukkan lebih sedikit kode ke dalam artikel, alih-alih saya menambahkan tautan ke baris kode tertentu di Github dengan harapan akan lebih nyaman untuk melihat keseluruhan gambar.


Pertama, saya membuat sketsa rencana apa yang harus ada dalam aplikasi. Karena saya akan berbicara tentang masing-masing item secara terpisah dalam artikel, saya pertama-tama akan memberikan yang utama dari daftar ini sebagai konten.


  • Pilih pengelola paket
  • Pilih kerangka kerja untuk membuat API
  • Pilih Alat untuk Injeksi Ketergantungan (DI)
  • Rute Permintaan Web
  • Respons JSON / XML sesuai dengan header permintaan
  • ORM
  • Migrasi
  • Buat kelas dasar untuk lapisan model Layanan-> Repositori-> Entitas
  • Repositori CRUD dasar
  • Layanan CRUD dasar
  • Pengontrol CRUD Dasar
  • Meminta Validasi
  • Konfigurasi dan variabel lingkungan
  • Perintah konsol
  • Penebangan
  • Integrasi logger dengan Sentry atau sistem peringatan lain
  • Menyetel peringatan untuk kesalahan
  • Tes unit dengan redefinisi layanan melalui DI
  • Persentase dan tes peta kode jangkauan
  • Kesombongan
  • Menulis Docker

Manajer paket


Setelah membaca deskripsi untuk berbagai implementasi, saya memilih govendor dan saat ini saya puas dengan pilihannya. Alasannya sederhana - ini memungkinkan Anda untuk menginstal dependensi di dalam direktori dengan aplikasi, menyimpan informasi tentang paket dan versinya.


Informasi tentang paket dan versinya disimpan dalam satu file vendor.json. Ada minus dalam pendekatan ini juga. Jika Anda menambahkan paket dengan dependensinya, maka bersama dengan informasi tentang paket tersebut, informasi tentang dependensinya juga akan masuk ke file. File tumbuh dengan cepat dan tidak lagi mungkin untuk menentukan dengan jelas dependensi mana yang utama dan turunannya.


Dalam komposer PHP atau dalam npm, dependensi utama dijelaskan dalam satu file, dan semua dependensi utama dan turunan serta versinya direkam secara otomatis dalam file kunci. Pendekatan ini lebih nyaman menurut saya. Tetapi untuk saat ini, implementasi govendor sudah cukup bagi saya.


Kerangka kerja


Dari kerangka saya tidak perlu banyak, router yang nyaman, validasi permintaan. Semua ini ditemukan di Gin yang populer. Dia berhenti di situ.


Ketergantungan injeksi


Dengan DI aku harus sedikit menderita. Pertama-tama pilih Dig. Dan pada awalnya semuanya bagus. Layanan yang dijelaskan, Menggali lebih jauh membangun dependensi, dengan nyaman. Tetapi kemudian ternyata layanan tidak dapat didefinisikan ulang, misalnya, selama pengujian. Oleh karena itu, pada akhirnya, saya sampai pada kesimpulan bahwa saya mengambil sarulab / di wadah layanan sederhana.


Saya baru saja harus memotongnya, karena di luar kotak itu memungkinkan Anda untuk menambahkan layanan dan melarang mendefinisikan ulang mereka. Dan ketika menulis autotests, menurut pendapat saya, lebih mudah untuk menginisialisasi wadah seperti dalam aplikasi, dan kemudian mendefinisikan kembali beberapa layanan, menentukan bertopik sebagai gantinya. Dalam fork, ia menambahkan metode untuk mengganti deskripsi layanan.


Tetapi pada akhirnya, baik dalam kasus Dig dan dalam wadah layanan, saya harus memasukkan tes ke dalam paket terpisah. Kalau tidak, ternyata tes dijalankan secara terpisah dalam paket ( go test model/service ), tetapi tes tersebut tidak segera dimulai untuk seluruh aplikasi ( go test ./... ), karena ketergantungan siklus yang muncul.


Respons JSON / XML sesuai dengan header permintaan


Di Gin, saya tidak menemukan ini, jadi saya hanya menambahkan metode ke pengontrol dasar yang menghasilkan respons tergantung pada header permintaan.


 func (c BaseController) response(context *gin.Context, obj interface{}, code int) { switch context.GetHeader("Accept") { case "application/xml": context.XML(code, obj) default: context.JSON(code, obj) } } 

ORM


Dengan ORM tidak merasakan siksaan panjang pilihan. Ada banyak pilihan. Tetapi menurut deskripsi fitur, saya suka GORM, yang merupakan salah satu yang paling populer pada saat pemilihan. Ada dukungan untuk DBMS yang paling umum digunakan. Setidaknya PostgreSQL dan MySQL ada di sana. Ini juga memiliki metode untuk mengelola skema dasar yang dapat Anda gunakan saat membuat migrasi.


Migrasi


Untuk migrasi, saya memilih paket angsa-angsa . Saya meletakkan paket terpisah secara global dan mulai migrasi ke sana. Pada awalnya, implementasi seperti itu malu, karena koneksi ke database harus dijelaskan dalam file db / dbconf.yml yang terpisah. Namun kemudian ternyata string koneksi di dalamnya dapat dideskripsikan sedemikian rupa sehingga nilainya diambil dari variabel lingkungan.


 development: driver: postgres open: $DB_URL 

Dan ini cukup nyaman. Paling tidak dengan docker-compose, saya tidak perlu menduplikasi string koneksi .


Gorm-angsa juga mendukung rollback migrasi, yang menurut saya sangat berguna.


Repositori CRUD dasar


Saya lebih suka segala sesuatu yang merujuk pada sumber daya untuk ditempatkan di lapisan repositori yang terpisah. Menurut pendapat saya, dengan pendekatan ini, kode logika bisnis lebih bersih. Dalam hal ini, kode logika bisnis hanya tahu bahwa kode itu perlu bekerja dengan data yang diambil dari repositori. Dan apa yang terjadi di repositori, logika bisnis tidak penting. Repositori dapat bekerja dengan database relasional, dengan penyimpanan KV, dengan disk, atau mungkin dengan API layanan lain. Kode logika bisnis akan sama dalam semua kasus ini.


Repositori CRUD mengimplementasikan antarmuka berikut


 type CrudRepositoryInterface interface { BaseRepositoryInterface GetModel() (entity.InterfaceEntity) Find(id uint) (entity.InterfaceEntity, error) List(parameters ListParametersInterface) (entity.InterfaceEntity, error) Create(item entity.InterfaceEntity) entity.InterfaceEntity Update(item entity.InterfaceEntity) entity.InterfaceEntity Delete(id uint) error } 

Yaitu, CRUD mengimplementasikan operasi Create() , Find() , List() , Update() , Delete() dan metode GetModel() .


Tentang GetModel () . Ada repositori CrudRepository dasar yang mengimplementasikan operasi CRUD dasar. Dalam repositori yang menanamkannya ke dalam diri mereka sendiri, cukup untuk menunjukkan model mana yang harus mereka gunakan. Untuk melakukan ini, metode GetModel() harus mengembalikan model GORM. Kemudian kami harus menggunakan hasil GetModel() menggunakan refleksi dalam metode CRUD.


Sebagai contoh


 func (c CrudRepository) Find(id uint) (entity.InterfaceEntity, error) { item := reflect.New(reflect.TypeOf(c.GetModel()).Elem()).Interface() err := c.db.First(item, id).Error return item, err } 

Pada kenyataannya, dalam hal ini perlu untuk meninggalkan pengetikan statis demi pengetikan dinamis. Pada saat-saat seperti itu, kurangnya obat generik dalam bahasa tersebut sangat terasa.


Agar repositori yang bekerja dengan model tertentu untuk menerapkan aturan mereka sendiri untuk memfilter daftar dalam metode List() , saya pertama kali menerapkan pengikatan terlambat sehingga metode yang bertanggung jawab untuk membangun kueri dipanggil dari metode List() . Dan metode ini dapat diimplementasikan dalam repositori tertentu. Sulit untuk meninggalkan pola pikir yang terbentuk ketika bekerja dengan bahasa lain. Tetapi, melihatnya dengan tampilan yang segar, dan menghargai "keanggunan" dari jalan yang dipilih, kemudian ia mengubahnya menjadi pendekatan yang lebih dekat dengan Go. Untuk melakukan ini, cukup di CrudRepository melalui antarmuka pembuat kueri dideklarasikan, yang sudah List() .


 listQueryBuilder ListQueryBuilderInterface 

Ternyata cukup lucu. Membatasi bahasa untuk mengikat terlambat, yang pada awalnya tampak seperti cacat, mendorong pemisahan kode yang lebih jelas.


Layanan CRUD dasar


Tidak ada yang menarik di sini, karena tidak ada logika bisnis dalam kerangka ini. Panggilan metode CRUD ke repositori hanya diproksi .


Di lapisan layanan, logika bisnis harus diimplementasikan.


Pengontrol CRUD Dasar


Pengontrol mengimplementasikan metode CRUD . Mereka memproses parameter dari permintaan, kontrol ditransfer ke metode layanan yang sesuai, dan berdasarkan respons layanan, respons dibentuk untuk klien.


Dengan controller saya punya cerita yang sama dengan repositori mengenai daftar penyaringan. Akibatnya, saya redid implementasi dengan pengikatan akhir buatan sendiri dan menambahkan hydrator , yang, berdasarkan parameter permintaan, membentuk struktur dengan parameter untuk memfilter daftar.


Dalam hydrator yang datang dengan kontroler CRUD, hanya parameter pagination yang diproses. Pada pengontrol spesifik di mana pengontrol CRUD terintegrasi, hydrator dapat didefinisikan ulang .


Meminta Validasi


Validasi dilakukan oleh Gin. Misalnya, ketika menambahkan catatan ( Create() metode), itu sudah cukup untuk menghiasi elemen struktur entitas


 Name string `binding:"required"` 

Metode framework ShouldBindJSON() menangani memeriksa parameter permintaan untuk kepatuhan dengan persyaratan yang dijelaskan dalam dekorator.


Konfigurasi dan variabel lingkungan


Saya sangat menyukai implementasi Viper , terutama dalam hubungannya dengan Cobra.


Membaca konfigurasi yang saya jelaskan di main.go. Parameter dasar yang tidak mengandung rahasia dijelaskan dalam file base.env . Anda dapat menimpanya di file .env yang ditambahkan ke .gitignore. Di .env, Anda bisa mendeskripsikan nilai-nilai rahasia untuk lingkungan.


Variabel lingkungan memiliki prioritas yang lebih tinggi.


Perintah konsol


Untuk deskripsi perintah konsol, saya memilih Cobra . Daripada itu baik untuk menggunakan Cobra bersama dengan Viper. Kita bisa menggambarkan perintahnya


 serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 

Dan ikat variabel lingkungan ke nilai parameter perintah


 viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 

Bahkan, seluruh aplikasi kerangka kerja ini adalah konsol. Server web diluncurkan oleh salah satu perintah konsol server.


 gin -i run server 

Penebangan


Saya memilih paket logrus untuk logging , karena ada semua yang biasanya saya butuhkan: mengatur level logging, tempat untuk login, menambahkan kait, misalnya, untuk mengirim log ke sistem peringatan.


Integrasi Logger dengan Sistem Peringatan


Saya memilih Sentry, karena semuanya ternyata cukup sederhana berkat integrasi siap pakai dengan logrus: logrus_sentry . Saya membuat parameter dengan url ke Sentry SENTRY_DSN dan batas waktu untuk mengirim ke Sentry SENTRY_TIMEOUT . Ternyata secara default, batas waktu kecil, jika tidak salah, 300 ms, dan banyak pesan tidak terkirim.


Menyetel peringatan untuk kesalahan


Saya melakukan pemrosesan panik secara terpisah untuk server web dan untuk perintah konsol .


Tes unit dengan redefinisi layanan melalui DI


Seperti disebutkan di atas, paket terpisah harus dialokasikan untuk pengujian unit. Karena pustaka yang dipilih untuk membuat wadah layanan tidak memungkinkan mendefinisikan ulang layanan, dalam garpu menambahkan metode untuk mendefinisikan kembali deskripsi layanan. Karena ini, dalam pengujian unit, Anda dapat menggunakan deskripsi layanan yang sama seperti dalam aplikasi


 dic.InitBuilder() 

Dan mendefinisikan ulang hanya beberapa deskripsi layanan di bertopik dengan cara ini


 dic.Builder.Set(di.Def{ Name: dic.UserRepository, Build: func(ctn di.Container) (interface{}, error) { return NewUserRepositoryMock(), nil }, }) 

Selanjutnya, Anda dapat membangun wadah dan menggunakan layanan yang diperlukan dalam tes:


 dic.Container = dic.Builder.Build() userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface) 

Dengan demikian, kami akan menguji userService, yang alih-alih repositori nyata akan menggunakan rintisan yang disediakan.


Persentase dan tes peta kode jangkauan
Saya benar-benar puas dengan utilitas uji go standar.


Anda dapat menjalankan tes secara individual


 go test test/unit/user_service_test.go -v 

Anda dapat menjalankan semua tes sekaligus


 go test ./... -v 

Anda dapat membangun peta cakupan dan menghitung persentase cakupan


 go test ./... -v -coverpkg=./... -coverprofile=coverage.out 

Dan lihat peta cakupan kode dengan tes di browser


 go tool cover -html=coverage.out 

Kesombongan


Ada proyek gin-swagger untuk Gin, yang dapat digunakan baik untuk menghasilkan spesifikasi untuk Swagger dan untuk menghasilkan dokumentasi berdasarkan itu. Tetapi, ternyata, untuk menghasilkan spesifikasi untuk operasi tertentu, perlu untuk menunjukkan komentar tentang fungsi spesifik dari pengontrol. Ini ternyata sangat tidak nyaman bagi saya, karena saya tidak ingin menduplikasi kode operasi CRUD di setiap controller. Sebagai gantinya, pada pengontrol tertentu, saya cukup menyematkan pengontrol CRUD seperti dijelaskan di atas. Saya juga tidak benar-benar ingin membuat fungsi rintisan.


Oleh karena itu, saya sampai pada kesimpulan bahwa spesifikasi dihasilkan menggunakan goswagger , karena dalam hal ini operasi dapat dijelaskan tanpa terikat dengan fungsi tertentu .


 swagger generate spec -o doc/swagger.yml 

Ngomong-ngomong, dengan goswagger Anda bahkan bisa pergi dari kebalikannya, dan menghasilkan kode server web berdasarkan spesifikasi Swagger. Tetapi dengan pendekatan ini, ada kesulitan dalam menggunakan ORM, dan saya akhirnya meninggalkannya.


Dokumentasi dibuat menggunakan gin-swagger, untuk ini file spesifikasi yang dihasilkan sebelumnya ditunjukkan .


Menulis Docker


Dalam kerangka itu, saya menambahkan deskripsi dua kontainer - untuk kode dan untuk pangkalan . Di awal wadah dengan kode, kami menunggu sampai wadah dengan pangkalan diluncurkan sepenuhnya. Dan pada setiap awal, kami menggulir migrasi jika perlu. Parameter untuk menghubungkan ke database untuk migrasi dijelaskan, seperti yang disebutkan di atas, di dbconf.yml , di mana dimungkinkan untuk menggunakan variabel lingkungan untuk mentransfer pengaturan untuk menghubungkan ke database.


Terima kasih atas perhatian anda Dalam prosesnya, saya harus beradaptasi dengan fitur bahasa. Saya akan tertarik untuk mengetahui pendapat rekan-rekan yang menghabiskan lebih banyak waktu dengan Go. Tentunya beberapa momen bisa dibuat lebih elegan, jadi saya akan senang menerima kritik yang berguna. Tautan ke bingkai: https://github.com/zubroide/go-api-boilerplate

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


All Articles