├── design-pattern ├── singleton.md ├── dependency-injection.md ├── concurrency-pattern.md ├── rate-limit.md ├── future-promise.md ├── semaphore.md ├── single-flight.md └── worker-pool.md ├── grpc-framework ├── grpc-caching.md ├── grpc-testing.md ├── pengenalan-grpc.md ├── makefile.md ├── grpc-tracing.md ├── grpc-protobuf.md ├── grpc-server.md ├── grpc-config.md ├── grpc-client.md ├── grpc-routing.md ├── grpc-clean-architecture.md └── grpc-database.md ├── graphql-framework └── pengenalan-graphql.md ├── .gitbook └── assets │ └── clean-architecture.jpg ├── SUMMARY.md ├── README.md └── golang-fundamental ├── pseudo_oop.md ├── basic.md └── konkurensi.md /design-pattern/singleton.md: -------------------------------------------------------------------------------- 1 | # Singleton 2 | 3 | -------------------------------------------------------------------------------- /grpc-framework/grpc-caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | -------------------------------------------------------------------------------- /grpc-framework/grpc-testing.md: -------------------------------------------------------------------------------- 1 | # gRPC Testing 2 | 3 | -------------------------------------------------------------------------------- /grpc-framework/pengenalan-grpc.md: -------------------------------------------------------------------------------- 1 | # Pengenalan gRPC 2 | 3 | -------------------------------------------------------------------------------- /design-pattern/dependency-injection.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | 3 | -------------------------------------------------------------------------------- /graphql-framework/pengenalan-graphql.md: -------------------------------------------------------------------------------- 1 | # Pengenalan GraphQL 2 | 3 | -------------------------------------------------------------------------------- /.gitbook/assets/clean-architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacky-htg/golang-essentials/HEAD/.gitbook/assets/clean-architecture.jpg -------------------------------------------------------------------------------- /grpc-framework/makefile.md: -------------------------------------------------------------------------------- 1 | # makefile 2 | 3 | * Buat file makefile 4 | 5 | ```text 6 | init: 7 | go mod init skeleton 8 | 9 | gen: 10 | protoc --proto_path=proto --go_out=paths=source_relative,plugins=grpc:./pb proto/*/*.proto 11 | 12 | .PHONY: init gen 13 | ``` 14 | 15 | * install [protoc](https://grpc.io/docs/protoc-installation/) 16 | * Uji installasi protoc `protoc --version` 17 | * jalankan perintah berikut: 18 | 19 | ```text 20 | make init 21 | mkdir pb 22 | make gen 23 | ``` 24 | 25 | * jika terjadi error `makefile:2: *** missing separator. Stop.` Hal itu mungkin disebabkan karena copy paste. Pastikan jarak indentasi merupakan tab, bukan spasi. 26 | 27 | -------------------------------------------------------------------------------- /grpc-framework/grpc-tracing.md: -------------------------------------------------------------------------------- 1 | # Tracing 2 | 3 | * Tracing penting untuk mencatat setiao request yang masuk. 4 | * Gunakan opentracing agar lebih fleksible. 5 | * Tambahkan env untuk tracing di file .env 6 | 7 | ```text 8 | PORT = 7070 9 | POSTGRES_HOST = localhost 10 | POSTGRES_PORT = 5432 11 | POSTGRES_USER = postgres 12 | POSTGRES_PASSWORD = pass 13 | POSTGRES_DB = drivers 14 | AUTH_SERVICE = localhost:5050 15 | SERVICE_NAME = skeleton 16 | DD_AGENT_HOST = localhost 17 | ``` 18 | 19 | * Update file server.go untuk memasang opentracing 20 | 21 | ```go 22 | t := opentracer.New( 23 | tracer.WithServiceName(os.Getenv("SERVICE_NAME")), 24 | tracer.WithAnalytics(true), 25 | tracer.WithAgentAddr(os.Getenv("DD_AGENT_HOST")), 26 | ) 27 | opentracing.SetGlobalTracer(t) 28 | defer tracer.Stop() 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /grpc-framework/grpc-protobuf.md: -------------------------------------------------------------------------------- 1 | # Protocol Buffer 2 | 3 | * Buat file proto/generic/generic\_message.proto 4 | 5 | ```text 6 | syntax = "proto3"; 7 | package skeleton; 8 | 9 | option go_package = "skeleton/pb/generic;generic"; 10 | 11 | message EmptyMessage {} 12 | message Id { 13 | string id = 1; 14 | } 15 | 16 | message StringMessage { 17 | string message = 1; 18 | } 19 | 20 | message BoolMessage { 21 | bool is_true = 1; 22 | } 23 | 24 | message Pagination { 25 | uint32 limit = 1; 26 | uint32 offset = 2; 27 | string keyword = 3; 28 | string order = 4; 29 | string sort = 5; 30 | } 31 | ``` 32 | 33 | * Buat file proto/drivers/driver\_message.proto 34 | 35 | ```text 36 | syntax = "proto3"; 37 | package skeleton; 38 | 39 | option go_package = "skeleton/pb/drivers;drivers"; 40 | 41 | message Driver { 42 | string id = 1; 43 | string name = 2; 44 | string phone = 3; 45 | string licence_number = 4; 46 | string company_id = 5; 47 | string company_name = 6; 48 | bool is_delete = 7; 49 | string created = 8; 50 | string created_by = 9; 51 | string updated = 10; 52 | string updated_by = 11; 53 | } 54 | 55 | message Drivers { 56 | repeated Driver driver = 1; 57 | } 58 | ``` 59 | 60 | * Buat file proto/drivers/driver\_input.proto 61 | 62 | ```text 63 | syntax = "proto3"; 64 | package skeleton; 65 | 66 | option go_package = "skeleton/pb/drivers;drivers"; 67 | 68 | import "generic/generic_message.proto"; 69 | 70 | message DriverListInput { 71 | repeated string ids = 1; 72 | repeated string names = 2; 73 | repeated string phones = 3; 74 | repeated string licence_numbers = 4; 75 | repeated string company_ids = 5; 76 | Pagination pagination = 6; 77 | } 78 | ``` 79 | 80 | * Buat file proto/drivers/driver\_service.proto 81 | 82 | ```text 83 | syntax = "proto3"; 84 | package skeleton; 85 | 86 | option go_package = "skeleton/pb/drivers;drivers"; 87 | 88 | import "drivers/driver_message.proto"; 89 | import "drivers/driver_input.proto"; 90 | import "generic/generic_message.proto"; 91 | 92 | service DriversService { 93 | rpc List(DriverListInput) returns (Drivers) {} 94 | rpc Create(Driver) returns (Driver) {} 95 | rpc Update(Driver) returns (Driver) {} 96 | rpc Delete(Id) returns (BoolMessage) {} 97 | } 98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Go Guidance](README.md) 4 | 5 | ## Golang Fundamental 6 | 7 | * [Basic Golang](golang-fundamental/basic.md) 8 | * [Pseudo OOP](golang-fundamental/pseudo_oop.md) 9 | * [Konkurensi](golang-fundamental/konkurensi.md) 10 | 11 | ## Design Pattern 12 | 13 | * [Singleton](design-pattern/singleton.md) 14 | * [Dependency Injection](design-pattern/dependency-injection.md) 15 | * [Concurrency Pattern](design-pattern/concurrency-pattern.md) 16 | - [Worker Pool](design-pattern/worker-pool.md) 17 | - [Future / Promise](design-pattern/future-promise.md) 18 | - [Rate Limit Pattern](design-pattern/rate-limit.md) 19 | - [Semaphore](design-pattern/semaphore.md) 20 | - [Single Flight](design-pattern/single-flight.md) 21 | 22 | ## Build Rest API Framework 23 | 24 | * [Pengenalan Rest API](build-rest-api-framework/pengenalan-rest-api.md) 25 | * [Start up](build-rest-api-framework/start-up.md) 26 | * [Shutdown](build-rest-api-framework/shutdown.md) 27 | * [Json](build-rest-api-framework/json.md) 28 | * [Database](build-rest-api-framework/database.md) 29 | * [Clean architecture](build-rest-api-framework/clean-architecture.md) 30 | * [Configuration](build-rest-api-framework/configuration.md) 31 | * [Fatal](build-rest-api-framework/fatal.md) 32 | * [Logging](build-rest-api-framework/logging.md) 33 | * [Routing](build-rest-api-framework/routing.md) 34 | * [CRUD](build-rest-api-framework/crud.md) 35 | * [Request Response Helper](build-rest-api-framework/request-response-helper.md) 36 | * [Error Handler](build-rest-api-framework/error-handler.md) 37 | * [Unit Testing](build-rest-api-framework/unit-testing.md) 38 | * [API Testing](build-rest-api-framework/api-testing.md) 39 | * [Context](build-rest-api-framework/context.md) 40 | * [Validation](build-rest-api-framework/validation.md) 41 | * [Middleware](build-rest-api-framework/middleware.md) 42 | * [Token](build-rest-api-framework/token.md) 43 | * [Role Based Access Controller](build-rest-api-framework/rbac.md) 44 | 45 | ## GraphQL Framework 46 | 47 | * [Pengenalan GraphQL](graphql-framework/pengenalan-graphql.md) 48 | 49 | ## gRPC Framework 50 | 51 | * [Pengenalan gRPC](grpc-framework/pengenalan-grpc.md) 52 | * [Protocol Buffer](grpc-framework/grpc-protobuf.md) 53 | * [makefile](grpc-framework/makefile.md) 54 | * [gRPC Server](grpc-framework/grpc-server.md) 55 | * [Config](grpc-framework/grpc-config.md) 56 | * [Database](grpc-framework/grpc-database.md) 57 | * [Routing](grpc-framework/grpc-routing.md) 58 | * [Clean Architecture](grpc-framework/grpc-clean-architecture.md) 59 | * [gRPC Client](grpc-framework/grpc-client.md) 60 | * [Tracing](grpc-framework/grpc-tracing.md) 61 | * [Caching](grpc-framework/grpc-caching.md) 62 | * [gRPC Testing](grpc-framework/grpc-testing.md) 63 | 64 | -------------------------------------------------------------------------------- /grpc-framework/grpc-server.md: -------------------------------------------------------------------------------- 1 | # gRPC Server 2 | 3 | * Buat file server.go 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "context" 10 | "log" 11 | "net" 12 | "os" 13 | "skeleton/pb/drivers" 14 | "skeleton/pb/generic" 15 | 16 | "google.golang.org/grpc" 17 | ) 18 | 19 | func main() { 20 | 21 | log := log.New(os.Stdout, "grpc skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 22 | 23 | // listen tcp port 24 | lis, err := net.Listen("tcp", ":7070") 25 | if err != nil { 26 | log.Fatalf("failed to listen: %v", err) 27 | return 28 | } 29 | 30 | grpcServer := grpc.NewServer() 31 | 32 | // routing grpc services 33 | grpcRoute(grpcServer, log) 34 | 35 | if err := grpcServer.Serve(lis); err != nil { 36 | log.Fatalf("failed to serve: %s", err) 37 | return 38 | } 39 | log.Print("serve grpc on port: 7070") 40 | 41 | } 42 | 43 | func grpcRoute(grpcServer *grpc.Server, log *log.Logger) { 44 | driverServer := newDriverHandler(log) 45 | 46 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 47 | } 48 | 49 | type driverHandler struct { 50 | log *log.Logger 51 | } 52 | 53 | func newDriverHandler(log *log.Logger) *driverHandler { 54 | handler := new(driverHandler) 55 | handler.log = log 56 | return handler 57 | } 58 | 59 | func (u *driverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 60 | return &drivers.Drivers{}, nil 61 | } 62 | 63 | func (u *driverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 64 | return in, nil 65 | } 66 | 67 | func (u *driverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 68 | return in, nil 69 | } 70 | 71 | func (u *driverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 72 | return &generic.BoolMessage{}, nil 73 | } 74 | ``` 75 | 76 | * Buat grpc server `grpcServer := grpc.NewServer()` 77 | * grpcRoute\(\) untuk handling routing grpc 78 | * Buat struct driverHandler yang mengimplementasikan seluruh interface DriverService protobuf 79 | * jalankan `go run server.go` 80 | 81 | ## call grpc dengan grpc client 82 | 83 | * ada banyak tool grpc client. ada yang berbasis gui seperti wombat maupun yang berbasis cli seperti grpcurl 84 | * [https://github.com/fullstorydev/grpcurl](https://github.com/fullstorydev/grpcurl) 85 | * setelah instal grpcurl, call grpc list driver dengan perintah : `grpcurl -import-path ~/jackyhtg/skeleton/proto -proto ~/jackyhtg/skeleton/proto/drivers/driver_service.proto -plaintext localhost:7070 skeleton.DriversService.List` 86 | * call grpc create driver 87 | 88 | `grpcurl -plaintext -import-path ~/jackyhtg/skeleton/proto -proto ~/jackyhtg/skeleton/proto/drivers/driver_service.proto -d '{"name": "jacky", "phone": "08172221", "licence_number": "1234", "company_id": "UAT", "company_name": "Universal Alabama Tahoma"}' localhost:7070 skeleton.DriversService.Create` 89 | 90 | -------------------------------------------------------------------------------- /grpc-framework/grpc-config.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | * kita sudah mempelajari config di materi [Configuration](../build-rest-api-framework/configuration.md) 4 | * Kita akan praktekan dengan mengubah port menjadi env. 5 | * Buat file config/config.go 6 | 7 | ```go 8 | package config 9 | 10 | import ( 11 | "io/ioutil" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | //Setup environment from file .env 17 | func Setup(file string) error { 18 | data, err := ioutil.ReadFile(file) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | datas := strings.Split(string(data), "\n") 24 | for _, env := range datas { 25 | e := strings.Split(env, "=") 26 | if len(e) >= 2 { 27 | os.Setenv(strings.TrimSpace(e[0]), strings.TrimSpace(strings.Join(e[1:], "="))) 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | ``` 34 | 35 | * Buat file .env 36 | 37 | ```text 38 | PORT=7070 39 | ``` 40 | 41 | * Update file server.go untuk menambahkan import "skeleton/config" 42 | * masih di file server.go pada fungsi main tambahkan di baris paling atas `config.Setup(".env")` 43 | * semua port yang dihardcode ganti dengan `os.Getenv("PORT")` 44 | 45 | ```go 46 | package main 47 | 48 | import ( 49 | "context" 50 | "log" 51 | "net" 52 | "os" 53 | "skeleton/config" 54 | "skeleton/pb/drivers" 55 | "skeleton/pb/generic" 56 | 57 | "google.golang.org/grpc" 58 | ) 59 | 60 | func main() { 61 | config.Setup(".env") 62 | 63 | log := log.New(os.Stdout, "Essentials : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 64 | 65 | // listen tcp port 66 | lis, err := net.Listen("tcp", ":"+os.Getenv("PORT")) 67 | if err != nil { 68 | log.Fatalf("failed to listen: %v", err) 69 | return 70 | } 71 | 72 | grpcServer := grpc.NewServer() 73 | 74 | // routing grpc services 75 | grpcRoute(grpcServer, log) 76 | 77 | if err := grpcServer.Serve(lis); err != nil { 78 | log.Fatalf("failed to serve: %s", err) 79 | return 80 | } 81 | log.Print("serve grpc on port: " + os.Getenv("PORT")) 82 | 83 | } 84 | 85 | func grpcRoute(grpcServer *grpc.Server, log *log.Logger) { 86 | driverServer := newDriverHandler(log) 87 | 88 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 89 | } 90 | 91 | type driverHandler struct { 92 | log *log.Logger 93 | } 94 | 95 | func newDriverHandler(log *log.Logger) *driverHandler { 96 | handler := new(driverHandler) 97 | handler.log = log 98 | return handler 99 | } 100 | 101 | func (u *driverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 102 | return &drivers.Drivers{}, nil 103 | } 104 | 105 | func (u *driverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 106 | return in, nil 107 | } 108 | 109 | func (u *driverHandler) Update(ctx context.Context, in *generic.Id) (*drivers.Driver, error) { 110 | return &drivers.Driver{}, nil 111 | } 112 | 113 | func (u *driverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 114 | return &generic.BoolMessage{}, nil 115 | } 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Guidance 2 | 3 | Mempelajari pemrograman golang untuk pemula. Materi akan dibahas step by step dari basic sampai mahir. Ruang lingkup pembelajaran meliputi : 4 | 5 | ## [Basic](golang-fundamental/basic.md) 6 | 7 | * Install golang 8 | * Hello world 9 | * Package, type, constanta, variable, function 10 | * Flow controll : if, else, switch, for, defer 11 | * Array : array, slice, map 12 | 13 | ## [Psuedo OOP](golang-fundamental/pseudo_oop.md) 14 | 15 | * struct 16 | * Method 17 | * Interface 18 | * Encapsulation, inheritance and polymorphism 19 | 20 | ## [Konkurensi](golang-fundamental/konkurensi.md) 21 | 22 | * Go routine 23 | * Channel 24 | * Channel dengan buffer 25 | * Range dan close 26 | * Select 27 | * Select default 28 | * Select timeout 29 | * Sync Mutex 30 | * Handling sync group routine 31 | 32 | ## Design Pattern 33 | 34 | * Singleton 35 | * Abstract factory 36 | * Dependency injection 37 | * [Concurrency pattern](design-pattern/concurrency-pattern.md) 38 | - [Worker Pool](design-pattern/worker-pool.md) 39 | - [Future / Promise](design-pattern/future-promise.md) 40 | - [Rate Limit](design-pattern/rate-limit.md) 41 | - [Semaphore](design-pattern/sempahore.md) 42 | - [Single Flight](design-pattern/single-flight.md) 43 | 44 | ## Build Rest API Framework 45 | 46 | Di materi sebelumnya, kita telah membuat project melalui perintah `go mod init essentials`. Jadi dalam project pembuatan framework API ini, kita memakai 'essentials' sebagai nama project. 47 | 48 | * [Start up](build-rest-api-framework/start-up.md) 49 | * [Shutdown](build-rest-api-framework/shutdown.md) 50 | * [Json](build-rest-api-framework/json.md) 51 | * [Database](build-rest-api-framework/database.md) 52 | * [Clean architecture](build-rest-api-framework/clean-architecture.md) 53 | * [Configuration](build-rest-api-framework/configuration.md) 54 | * [Fatal](build-rest-api-framework/fatal.md) 55 | * [Logging](build-rest-api-framework/logging.md) 56 | * [Routing](build-rest-api-framework/routing.md) 57 | * [CRUD](build-rest-api-framework/crud.md) 58 | * [Request-Response helper](build-rest-api-framework/request-response-helper.md) 59 | * [Error handler](build-rest-api-framework/error-handler.md) 60 | * [Unit testing](build-rest-api-framework/unit-testing.md) 61 | * [API testing](build-rest-api-framework/api-testing.md) 62 | * [Context](build-rest-api-framework/context.md) 63 | * [Validation](build-rest-api-framework/validation.md) 64 | * [Middleware](build-rest-api-framework/middleware.md) 65 | * [Token](build-rest-api-framework/token.md) 66 | * [RBAC](build-rest-api-framework/rbac.md) 67 | 68 | ## Build gRPC API Framework 69 | 70 | * [Protocol Buffer](grpc-framework/grpc-protobuf.md) 71 | * [makefile](grpc-framework/makefile.md) 72 | * [grpc server](grpc-framework/grpc-server.md) 73 | * [Config](grpc-framework/grpc-config.md) 74 | * [Database](grpc-framework/grpc-database.md) 75 | * [Routing](grpc-framework/grpc-routing.md) 76 | * [Clean Architecture](grpc-framework/grpc-clean-architecture.md) 77 | * [gRPC Client](grpc-framework/grpc-client.md) 78 | * [Tracing](grpc-framework/grpc-tracing.md) 79 | * [Caching](grpc-framework/grpc-caching.md) 80 | * [Testing](grpc-framework/grpc-testing.md) 81 | 82 | ## Referensi Tambahan 83 | 84 | * [Buku "The Go Programing Language"](https://www.gopl.io/) 85 | * [Dokumentasi Resmi Golang](https://golang.org/doc/) -------------------------------------------------------------------------------- /design-pattern/concurrency-pattern.md: -------------------------------------------------------------------------------- 1 | # Concurrency Pattern 2 | Untuk alasan performance, kita sering mengeksploitasi fitur konkurensi di golang. Ada beberapa pattern yang terkenal terkait konkurensi ini, beberapa diantaranya adalah : 3 | 4 | 5 | ## 1. [Worker Pool](worker-pool.md) 6 | 7 | - Membuat sejumlah worker (goroutine) tetap yang mengambil tugas dari sebuah job queue. 8 | - Digunakan untuk membatasi jumlah goroutine agar tidak membebani CPU/memori. 9 | 10 | Contoh Kasus: Pemrosesan antrian tugas di backend, seperti pemrosesan gambar atau request API. 11 | 12 | ## 2. Fan-Out, Fan-In 13 | 14 | - Fan-Out: Banyak goroutine dibuat untuk memproses data dari satu sumber. 15 | - Fan-In: Beberapa goroutine mengirimkan hasilnya ke satu channel untuk digabungkan. 16 | 17 | Contoh Kasus: Memproses banyak permintaan HTTP secara paralel, lalu menggabungkan hasilnya. 18 | 19 | ## 3. Publish-Subscribe (Pub-Sub) 20 | 21 | - Satu publisher mengirimkan pesan ke banyak subscriber. 22 | - Bisa dilakukan dengan channel atau message broker seperti Redis Pub/Sub atau Kafka. 23 | 24 | Contoh Kasus: Notifikasi real-time, event-driven architecture. 25 | 26 | ## 4. Pipeline 27 | 28 | - Data mengalir melalui beberapa tahap pemrosesan, di mana setiap tahap dilakukan oleh goroutine berbeda. 29 | - Setiap tahap beroperasi secara independen dengan channel sebagai perantara. 30 | 31 | Contoh Kasus: ETL (Extract, Transform, Load), pemrosesan data bertingkat. 32 | 33 | ## 5. [Future / Promise](future-promise.md) 34 | 35 | - Menggunakan goroutine untuk menjalankan tugas async dan mengembalikan hasilnya melalui channel atau struct yang menampung nilai dan status. 36 | 37 | Contoh Kasus: Menjalankan beberapa query database secara paralel dan menunggu hasilnya. 38 | 39 | ## 6. [Rate Limiting / Token Bucket](rate-limit.md) 40 | 41 | - Mengontrol jumlah goroutine atau request dalam periode waktu tertentu untuk mencegah overload. 42 | 43 | Contoh Kasus: Membatasi jumlah request API ke layanan eksternal. 44 | 45 | ## 7. [Semaphore](semaphore.md) 46 | 47 | - Menggunakan semaphoric channel untuk membatasi jumlah goroutine yang berjalan bersamaan. 48 | 49 | Contoh Kasus: Mengontrol akses ke sumber daya yang terbatas seperti koneksi database. 50 | 51 | ## 8. Balking Pattern 52 | 53 | - Jika suatu goroutine menemukan kondisi tertentu (misalnya, resource sedang dipakai), maka ia membatalkan tugasnya tanpa menunggu. 54 | 55 | Contoh Kasus: Cache warming, di mana hanya satu goroutine yang boleh memperbarui cache. 56 | 57 | ## 9. [Single Flight Pattern](single-flight.md) 58 | - Jika dalam waktu bersamaan ada beberapa permintaan identik yang masuk, maka hanya ada satu permintaan yang diteruskan, yang lainnya akan menunggu. Setelah permintaan yang diteruskan mendapatakan response, maka semua permintaan yang masuk akan menerima response yang sama. 59 | - Banyak digunakan untuk mengelola permintaan ke sebuah proses yang lambat/berat. 60 | 61 | Contoh Kasus: request reporting, request ke heavy database, request ke proses yang latency tinggi dan consume banyak resource (memory/cpu), call api third party yang lambat. 62 | 63 | ## 10. Circuit Breaker 64 | 65 | - Jika ada kegagalan berturut-turut, sistem akan berhenti mencoba untuk sementara waktu. 66 | - Bisa dikombinasikan dengan timeout atau retry pattern. 67 | 68 | Contoh Kasus: Mencegah request berulang ke layanan eksternal yang sedang down. -------------------------------------------------------------------------------- /design-pattern/rate-limit.md: -------------------------------------------------------------------------------- 1 | # Rate Limit Pattern 2 | 3 | Rate limiting adalah teknik untuk membatasi jumlah permintaan (requests) dalam periode waktu tertentu guna: 4 | - Mencegah penyalahgunaan API (misalnya DDoS atau brute force). 5 | - Melindungi performa server agar tidak overload. 6 | - Membagi resource secara adil di antara pengguna. 7 | 8 | Perbedaan Rate Limit dengan Semaphore: 9 | 10 | | Fitur | Rate Limit | Semaphore | 11 | | ---------- | ------------------------------------ | ------------------------------------------------------------- | 12 | | Membatasi | Jumlah request dalam waktu tertentu | Jumlah goroutine aktif | 13 | | Penerapan | Berbasis waktu (misal: 10 req/detik) | Berbasis concurrency (misal: 5 goroutine berjalan bersamaan) | 14 | | Penggunaan | API rate limiting | Kontrol parallelism | 15 | 16 | ## Jenis-Jenis Rate Limiting 17 | 18 | 1. Fixed Window → Memeriksa jumlah request dalam interval tetap (misal: 10 request per menit). 19 | 2. Sliding Window → Menghitung request dalam periode berjalan agar lebih akurat. 20 | 3. Token Bucket → Menggunakan token yang diisi secara periodik (misalnya, 10 token per detik, 1 request = 1 token). 21 | 4. Leaky Bucket → Request masuk dalam antrian, diproses secara tetap untuk menghindari lonjakan tiba-tiba. 22 | 23 | ## Implementasi Simple Rate Limit 24 | Berikut adalah implmentasi rate limit sederhana menggunakan token bucket. 25 | 26 | ```go 27 | package rate_limiter 28 | 29 | import ( 30 | "sync" 31 | "time" 32 | ) 33 | 34 | // RateLimiter menggunakan Token Bucket 35 | type RateLimiter struct { 36 | mu sync.Mutex 37 | rate int // Requests per second 38 | burst int // Maximum burst capacity 39 | tokens int // Available tokens 40 | lastChecked time.Time // Last refill time 41 | } 42 | 43 | // NewRateLimiter membuat RateLimiter baru 44 | func NewRateLimiter(rate, burst int) *RateLimiter { 45 | return &RateLimiter{ 46 | rate: rate, 47 | burst: burst, 48 | tokens: burst, 49 | lastChecked: time.Now(), 50 | } 51 | } 52 | 53 | // Allow mengecek apakah request bisa diproses 54 | func (rl *RateLimiter) Allow() bool { 55 | rl.mu.Lock() 56 | defer rl.mu.Unlock() 57 | 58 | now := time.Now() 59 | elapsed := now.Sub(rl.lastChecked).Seconds() 60 | rl.lastChecked = now 61 | 62 | // Tambah token berdasarkan waktu berlalu 63 | rl.tokens += int(elapsed * float64(rl.rate)) 64 | if rl.tokens > rl.burst { 65 | rl.tokens = rl.burst 66 | } 67 | 68 | // Jika masih ada token, izinkan request 69 | if rl.tokens > 0 { 70 | rl.tokens-- 71 | return true 72 | } 73 | 74 | return false 75 | } 76 | ``` 77 | 78 | Di middleware bisa memanggil paket rate limiter 79 | 80 | ```go 81 | package middleware 82 | 83 | import ( 84 | "log" 85 | "net/http" 86 | "myapp/rate_limiter" // Import dari package rate_limiter 87 | ) 88 | 89 | // RateLimitMiddleware middleware untuk membatasi request 90 | func RateLimitMiddleware(limiter *rate_limiter.RateLimiter) func(http.Handler) http.Handler { 91 | return func(next http.Handler) http.Handler { 92 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | if !limiter.Allow() { 94 | log.Println("Too many requests") 95 | http.Error(w, "Too Many Requests", http.StatusTooManyRequests) 96 | return 97 | } 98 | next.ServeHTTP(w, r) 99 | }) 100 | } 101 | } 102 | ``` 103 | 104 | ```go 105 | package main 106 | 107 | import ( 108 | "fmt" 109 | "myapp/middleware" 110 | "myapp/rate_limiter" 111 | "net/http" 112 | "time" 113 | ) 114 | 115 | func main() { 116 | // Rate limiter: 2 request per detik, max burst 5 117 | limiter := rate_limiter.NewRateLimiter(2, 5) 118 | 119 | // Middleware rate limit 120 | middleware := middleware.RateLimitMiddleware(limiter) 121 | 122 | // Handler utama 123 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | fmt.Fprintf(w, "Request processed at %s\n", time.Now().Format(time.RFC3339)) 125 | }) 126 | 127 | // Pasang middleware di server 128 | http.Handle("/", middleware(handler)) 129 | 130 | fmt.Println("Server running on port 8080") 131 | http.ListenAndServe(":8080", nil) 132 | } 133 | ``` 134 | 135 | ## Kesimpulan 136 | 137 | ✅ Rate Limiting berguna untuk: 138 | 139 | - Mencegah abuse/DDoS dengan membatasi jumlah request per waktu tertentu. 140 | - Menjaga performa server agar tidak overload. 141 | - Mengontrol penggunaan API agar lebih adil untuk semua pengguna. 142 | 143 | 🚀 Gunakan rate limiting jika Anda ingin membatasi jumlah request dalam periode waktu tertentu! -------------------------------------------------------------------------------- /design-pattern/future-promise.md: -------------------------------------------------------------------------------- 1 | # Future / Promise Pattern 2 | 3 | Future/Promise adalah pola pemrograman yang digunakan untuk menangani operasi asinkron dengan cara yang lebih terstruktur. Konsep ini mirip dengan async/await di JavaScript, di mana kita bisa menjalankan tugas secara non-blokir (non-blocking) dan mendapatkan hasilnya nanti setelah tugas selesai. 4 | 5 | ## Perbedaan Future/Promise dengan Goroutine & Channel di Go 6 | 7 | Di JavaScript, Promise atau async/await digunakan untuk menangani operasi asinkron dengan mudah. Sementara itu, di Go, tidak ada Promise secara langsung, tetapi konsep Future bisa diimplementasikan menggunakan goroutine dan channel. 8 | 9 | | Fitur | Promise (JS) | Future (Go - Custom) | 10 | | -------------- | --------------------- | ------------------------------ | 11 | | Cara Kerja | then(), await | Channel atau sync.WaitGroup | 12 | | Eksekusi | Non-blocking | Non-blocking | 13 | | Error Handling | catch() / try...catch | select { case <- errChan } | 14 | | Library Bawaan | Ya (Promise) | Tidak ada, harus dibuat manual | 15 | 16 | ## Implementasi Future di Go (Mirip dengan Promise di JavaScript) 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "time" 24 | ) 25 | 26 | // Future struct untuk menyimpan hasil async task 27 | type Future struct { 28 | result chan string 29 | } 30 | 31 | // AsyncFunction menjalankan tugas secara asinkron dan mengembalikan Future 32 | func AsyncFunction() *Future { 33 | f := &Future{result: make(chan string, 1)} 34 | 35 | go func() { 36 | time.Sleep(2 * time.Second) // Simulasi operasi yang butuh waktu lama 37 | f.result <- "Hasil dari Future!" 38 | }() 39 | 40 | return f 41 | } 42 | 43 | // Get akan mengambil hasil dari Future (mirip dengan await) 44 | func (f *Future) Get() string { 45 | return <-f.result 46 | } 47 | 48 | func main() { 49 | fmt.Println("Mulai tugas...") 50 | future := AsyncFunction() // Memulai tugas async tanpa blocking 51 | 52 | // Kita bisa melakukan hal lain di sini sementara tugas async berjalan 53 | fmt.Println("Melakukan tugas lain...") 54 | 55 | // Ambil hasil dari Future (mirip await) 56 | result := future.Get() 57 | fmt.Println("Hasil Future:", result) 58 | } 59 | ``` 60 | 61 | Output: 62 | 63 | ``` 64 | Mulai tugas... 65 | Melakukan tugas lain... 66 | (Hasil muncul setelah 2 detik) 67 | Hasil Future: Hasil dari Future! 68 | ``` 69 | 70 | ✅ Tidak blocking, tetap bisa menjalankan kode lain. 71 | 72 | ## Future dengan Error Handling (Mirip try...catch di Promise) 73 | 74 | ```go 75 | package main 76 | 77 | import ( 78 | "errors" 79 | "fmt" 80 | "time" 81 | ) 82 | 83 | // Future struct untuk menyimpan hasil dan error dari async task 84 | type Future struct { 85 | result chan string 86 | err chan error 87 | } 88 | 89 | // AsyncFunction menjalankan tugas secara asinkron dan mengembalikan Future 90 | func AsyncFunction() *Future { 91 | f := &Future{ 92 | result: make(chan string, 1), 93 | err: make(chan error, 1), 94 | } 95 | 96 | go func() { 97 | time.Sleep(2 * time.Second) // Simulasi delay 98 | 99 | if time.Now().Unix()%2 == 0 { 100 | f.result <- "Data sukses!" 101 | } else { 102 | f.err <- errors.New("Terjadi kesalahan dalam Future") 103 | } 104 | }() 105 | 106 | return f 107 | } 108 | 109 | // Get akan mengambil hasil dari Future (mirip dengan await) 110 | func (f *Future) Get() (string, error) { 111 | select { 112 | case res := <-f.result: 113 | return res, nil 114 | case err := <-f.err: 115 | return "", err 116 | } 117 | } 118 | 119 | func main() { 120 | fmt.Println("Mulai tugas...") 121 | future := AsyncFunction() 122 | 123 | fmt.Println("Melakukan tugas lain...") 124 | 125 | // Ambil hasil dari Future (mirip await) 126 | result, err := future.Get() 127 | if err != nil { 128 | fmt.Println("Error Future:", err) 129 | } else { 130 | fmt.Println("Hasil Future:", result) 131 | } 132 | } 133 | ``` 134 | 135 | ✅ Mirip try...catch di Promise, bisa menangani error dengan baik. 136 | 137 | ## Kapan Menggunakan Future/Promise di Go? 138 | 139 | 🚀 Future sangat berguna untuk: 140 | 141 | - Memanggil API secara asinkron → Misalnya fetching data dari third-party API tanpa memblokir eksekusi lainnya. 142 | - Mengurangi Blocking dalam Goroutine → Memungkinkan eksekusi tetap berjalan tanpa menunggu satu tugas selesai duluan. 143 | - Meningkatkan Performa → Membantu menangani pekerjaan berat seperti query database atau proses perhitungan besar tanpa menghentikan alur program. 144 | 145 | ## Kesimpulan 146 | 147 | - Future/Promise di Go bisa dibuat menggunakan goroutine + channel untuk menjalankan operasi asinkron. 148 | - Mirip dengan JavaScript async/await, tetapi tidak built-in, harus dibuat manual. 149 | - Future bisa menangani error dengan select { case result <- chan, case err <- chan }. 150 | - Sangat bermanfaat untuk pemanggilan API, query database, dan tugas berat lainnya tanpa memblokir eksekusi. 151 | 152 | 🚀 Jika terbiasa dengan async/await di JavaScript, Future di Go adalah cara terbaik untuk menulis kode asinkron yang lebih bersih dan efisien! -------------------------------------------------------------------------------- /design-pattern/semaphore.md: -------------------------------------------------------------------------------- 1 | # Semaphore Pattern 2 | 3 | Semaphore pattern adalah teknik dalam concurrent programming yang digunakan untuk mengontrol jumlah goroutine atau thread yang berjalan secara bersamaan. 4 | 5 | 🛠 Cara Kerja: 6 | 7 | - Semaphore memiliki batas maksimum (limit) untuk jumlah operasi yang berjalan bersamaan. 8 | - Ketika limit tercapai, goroutine berikutnya harus menunggu sampai ada slot yang tersedia. 9 | - Digunakan untuk mencegah overload atau resource starvation pada sistem. 10 | 11 | ## Contoh Dasar Implementasi Semaphore di Golang 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | func main() { 23 | const maxConcurrentJobs = 3 // Batas maksimal goroutine yang boleh berjalan 24 | semaphore := make(chan struct{}, maxConcurrentJobs) 25 | 26 | var wg sync.WaitGroup 27 | for i := 1; i <= 10; i++ { 28 | wg.Add(1) 29 | 30 | // Mengisi slot semaphore sebelum memulai pekerjaan 31 | semaphore <- struct{}{} 32 | 33 | go func(jobID int) { 34 | defer wg.Done() 35 | defer func() { <-semaphore }() // Melepaskan slot semaphore setelah selesai 36 | 37 | fmt.Printf("Processing job %d\n", jobID) 38 | time.Sleep(2 * time.Second) // Simulasi pekerjaan 39 | }(i) 40 | } 41 | 42 | wg.Wait() 43 | fmt.Println("All jobs completed") 44 | } 45 | ``` 46 | 47 | Penjelasan: 48 | 49 | - Membatasi jumlah goroutine aktif (dalam contoh ini, hanya 3 goroutine yang berjalan bersamaan). 50 | - Menunggu slot kosong jika jumlah goroutine yang berjalan sudah mencapai batas. 51 | - Mencegah aplikasi overload dengan terlalu banyak goroutine. 52 | 53 | Pada prakteknya, seringkali semaphore digunakan di middleware untuk mengontrol banyaknya request yang bisa dilayani secara bersamaan. 54 | 55 | ## Implementasi Middleware Semaphore untuk gRPC 56 | 57 | ```go 58 | package middleware 59 | 60 | import ( 61 | "context" 62 | 63 | "google.golang.org/grpc" 64 | "google.golang.org/grpc/codes" 65 | "google.golang.org/grpc/status" 66 | ) 67 | 68 | // Semaphore struct untuk membatasi jumlah request 69 | type Semaphore struct { 70 | sem chan struct{} 71 | } 72 | 73 | // NewSemaphore membuat middleware semaphore 74 | func NewSemaphore(maxConcurrentRequests int) *Semaphore { 75 | return &Semaphore{ 76 | sem: make(chan struct{}, maxConcurrentRequests), // Buffer menentukan batas maksimal request 77 | } 78 | } 79 | 80 | // UnaryInterceptor membatasi jumlah request secara global 81 | func (s *Semaphore) UnaryInterceptor() grpc.UnaryServerInterceptor { 82 | return func( 83 | ctx context.Context, 84 | req interface{}, 85 | info *grpc.UnaryServerInfo, 86 | handler grpc.UnaryHandler, 87 | ) (interface{}, error) { 88 | // Coba memasukkan slot ke semaphore 89 | select { 90 | case s.sem <- struct{}{}: 91 | // Pastikan slot dilepas setelah selesai 92 | defer func() { <-s.sem }() 93 | default: 94 | return nil, status.Error(codes.ResourceExhausted, "Too many concurrent requests") 95 | } 96 | 97 | // Lanjutkan ke handler utama 98 | return handler(ctx, req) 99 | } 100 | } 101 | ``` 102 | 103 | Cara Kerja Middleware gRPC Semaphore 104 | 105 | - Membatasi jumlah request yang masuk berdasarkan maxConcurrentRequests. 106 | - Jika slot penuh, request langsung ditolak dengan error ResourceExhausted. 107 | - Menggunakan channel sebagai semaphore untuk tracking request yang berjalan. 108 | 109 | ## Implementasi Middleware Semaphore untuk REST API (HTTP) 110 | 111 | ```go 112 | package middleware 113 | 114 | import ( 115 | "log" 116 | "net/http" 117 | ) 118 | 119 | // Semaphore struct untuk REST API 120 | type Semaphore struct { 121 | sem chan struct{} 122 | } 123 | 124 | // NewSemaphore membuat instance semaphore 125 | func NewSemaphore(maxConcurrentRequests int) *Semaphore { 126 | return &Semaphore{ 127 | sem: make(chan struct{}, maxConcurrentRequests), 128 | } 129 | } 130 | 131 | // Middleware membatasi jumlah request 132 | func (s *Semaphore) Middleware(next http.Handler) http.Handler { 133 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 134 | select { 135 | case s.sem <- struct{}{}: // Jika masih ada slot, lanjutkan 136 | defer func() { <-s.sem }() // Pastikan slot dilepas setelah selesai 137 | default: 138 | log.Println("Too many concurrent requests") 139 | http.Error(w, "Too many concurrent requests", http.StatusTooManyRequests) 140 | return 141 | } 142 | 143 | next.ServeHTTP(w, r) 144 | }) 145 | } 146 | ``` 147 | 148 | Cara Menggunakan Middleware di HTTP Server 149 | 150 | ```go 151 | package main 152 | 153 | import ( 154 | "fmt" 155 | "myapp/middleware" 156 | "net/http" 157 | "time" 158 | ) 159 | 160 | func main() { 161 | // Buat middleware semaphore dengan batas 3 request bersamaan 162 | sem := middleware.NewSemaphore(3) 163 | 164 | // Handler utama 165 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 | fmt.Fprintf(w, "Processing request...\n") 167 | time.Sleep(2 * time.Second) // Simulasi proses 168 | fmt.Fprintf(w, "Request completed!\n") 169 | }) 170 | 171 | // Pasang middleware 172 | http.Handle("/", sem.Middleware(handler)) 173 | 174 | fmt.Println("Server running on port 8080") 175 | http.ListenAndServe(":8080", nil) 176 | } 177 | ``` 178 | 179 | ## Kesimpulan 180 | 181 | ✅ Semaphore cocok di Middleware 182 | 183 | - gRPC: Gunakan UnaryInterceptor untuk batasi concurrent request di gRPC server. 184 | - REST API: Gunakan http.Handler middleware untuk batasi HTTP request di web server. 185 | 186 | ✅ Mengatasi Overload 187 | 188 | - Jika batas tercapai, request langsung ditolak dengan error Too many concurrent requests. 189 | 190 | ✅ Memastikan Performa Stabil 191 | 192 | - Dengan semaphore, server tidak overload meskipun banyak request masuk. 193 | 194 | 🚀 Gunakan middleware ini untuk melindungi API dari lonjakan traffic dan menjaga stabilitas server! 195 | -------------------------------------------------------------------------------- /grpc-framework/grpc-client.md: -------------------------------------------------------------------------------- 1 | # gRPC Client 2 | 3 | * Dalam micro servoces, memanggil service lain adalah sebuah keniscayaan. 4 | * Dengan asumsi semua service internal didevelop dengan grpc, maka kita perlu membuat panggilan grpc client. 5 | * Misal kita akan memanggil service auth yang mempunyai file proto proto/auth/auth\_service.proto 6 | 7 | ```text 8 | syntax = "proto3"; 9 | package skeleton; 10 | 11 | option go_package = "skeleton/pb/auth;auth"; 12 | 13 | message LoginInput { 14 | string username = 1; 15 | string password = 2; 16 | } 17 | 18 | message TokenResponse { 19 | string token = 1; 20 | } 21 | 22 | service AuthService { 23 | rpc Login(LoginInput) returns (TokenResponse) {} 24 | } 25 | ``` 26 | 27 | * jalankan `make gen` 28 | * Buat lib grpc client lib/grpcclient/client.go 29 | 30 | ```go 31 | package grpcclient 32 | 33 | import ( 34 | "google.golang.org/grpc" 35 | ) 36 | 37 | func Close(conn map[string]*grpc.ClientConn) { 38 | for _, c := range conn { 39 | c.Close() 40 | } 41 | } 42 | ``` 43 | 44 | * Buat file grpcconn/client\_conn.go 45 | 46 | ```go 47 | package grpcconn 48 | 49 | import ( 50 | "fmt" 51 | "os" 52 | "skeleton/lib/grpcclient" 53 | 54 | "google.golang.org/grpc" 55 | ) 56 | 57 | func ClientConn() (map[string]*grpc.ClientConn, func(), error) { 58 | conn := make(map[string]*grpc.ClientConn) 59 | 60 | authConn, err := grpc.Dial(os.Getenv("AUTH_SERVICE"), grpc.WithInsecure()) 61 | if err != nil { 62 | return nil, func() {}, fmt.Errorf("create auth service connection: %v", err) 63 | } 64 | 65 | conn["AUTH"] = authConn 66 | 67 | return conn, func() { grpcclient.Close(conn) }, nil 68 | } 69 | ``` 70 | 71 | * Kode di atas memanggil address auth service yang disimpan dalam env. 72 | * Tambahkan env auth service address di file .env 73 | 74 | ```text 75 | PORT = 7070 76 | POSTGRES_HOST = localhost 77 | POSTGRES_PORT = 5432 78 | POSTGRES_USER = postgres 79 | POSTGRES_PASSWORD = pass 80 | POSTGRES_DB = drivers 81 | AUTH_SERVICE = localhost:5050 82 | ``` 83 | 84 | * Update file server.go untuk membuat koneksi grpc client dan menginject-nya ke routing agar diteruskan ke service yang sekiranya membutuhkan koneksi tersebut. 85 | 86 | ```go 87 | package main 88 | 89 | import ( 90 | "log" 91 | "net" 92 | "os" 93 | 94 | "skeleton/config" 95 | "skeleton/grpcconn" 96 | "skeleton/lib/database/postgres" 97 | "skeleton/route" 98 | 99 | _ "github.com/lib/pq" 100 | "google.golang.org/grpc" 101 | ) 102 | 103 | func main() { 104 | config.Setup(".env") 105 | 106 | log := log.New(os.Stdout, "Skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 107 | 108 | db, err := postgres.Open() 109 | if err != nil { 110 | log.Fatalf("connecting to db: %v", err) 111 | return 112 | } 113 | log.Print("connecting to postgresql database") 114 | 115 | defer db.Close() 116 | 117 | // listen tcp port 118 | lis, err := net.Listen("tcp", ":"+os.Getenv("PORT")) 119 | if err != nil { 120 | log.Fatalf("failed to listen: %v", err) 121 | return 122 | } 123 | 124 | grpcServer := grpc.NewServer() 125 | 126 | clientConn, clientClose, err := grpcconn.ClientConn() 127 | if err != nil { 128 | log.Fatalf("failed to get grpc client connection: %v", err) 129 | return 130 | } 131 | defer clientClose() 132 | 133 | // routing grpc services 134 | route.GrpcRoute(grpcServer, log, db, clientConn) 135 | 136 | if err := grpcServer.Serve(lis); err != nil { 137 | log.Fatalf("failed to serve: %s", err) 138 | return 139 | } 140 | log.Print("serve grpc on port: " + os.Getenv("PORT")) 141 | 142 | } 143 | ``` 144 | 145 | * Ubah file route/route.go untuk menambahkan dependecy injection koneksi grpc client kepada repository yang membutuhkan. 146 | 147 | ```go 148 | package route 149 | 150 | import ( 151 | "database/sql" 152 | "log" 153 | 154 | driverHandler "skeleton/domain/ddrivers/handler" 155 | driverRepo "skeleton/domain/ddrivers/repositories" 156 | driverUsecase "skeleton/domain/ddrivers/usecase" 157 | "skeleton/pb/auth" 158 | "skeleton/pb/drivers" 159 | 160 | "google.golang.org/grpc" 161 | ) 162 | 163 | func GrpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB, clientConnection map[string]*grpc.ClientConn) { 164 | driverServer := driverHandler.NewDriverHandler( 165 | driverUsecase.NewService( 166 | log, 167 | driverRepo.NewDriverRepo(db, log, auth.NewAuthServiceClient(clientConnection["AUTH"]))), 168 | ) 169 | 170 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 171 | } 172 | ``` 173 | 174 | * Ubah file repository yang membutuhkan koneksi grpc client. misal file domain/ddrivers/repositories/repo.go 175 | 176 | ```go 177 | package repositories 178 | 179 | import ( 180 | "database/sql" 181 | "log" 182 | "skeleton/domain/ddrivers" 183 | "skeleton/pb/auth" 184 | "skeleton/pb/drivers" 185 | ) 186 | 187 | type repo struct { 188 | db *sql.DB 189 | log *log.Logger 190 | pb drivers.Driver 191 | authClient auth.AuthServiceClient 192 | } 193 | 194 | func NewDriverRepo(db *sql.DB, log *log.Logger, authClient auth.AuthServiceClient) ddrivers.DriverRepoInterface { 195 | return &repo{ 196 | db: db, 197 | log: log, 198 | authClient: authClient, 199 | } 200 | } 201 | 202 | func (u *repo) GetPb() *drivers.Driver { 203 | return &u.pb 204 | } 205 | 206 | func (u *repo) SetPb(in *drivers.Driver) { 207 | if len(in.Id) > 0 { 208 | u.pb.Id = in.Id 209 | } 210 | if len(in.Name) > 0 { 211 | u.pb.Name = in.Name 212 | } 213 | if len(in.Phone) > 0 { 214 | u.pb.Phone = in.Phone 215 | } 216 | if len(in.LicenceNumber) > 0 { 217 | u.pb.LicenceNumber = in.LicenceNumber 218 | } 219 | if len(in.CompanyId) > 0 { 220 | u.pb.CompanyId = in.CompanyId 221 | } 222 | if len(in.CompanyName) > 0 { 223 | u.pb.CompanyName = in.CompanyName 224 | } 225 | u.pb.IsDelete = in.IsDelete 226 | if len(in.Created) > 0 { 227 | u.pb.Created = in.Created 228 | } 229 | if len(in.CreatedBy) > 0 { 230 | u.pb.CreatedBy = in.CreatedBy 231 | } 232 | if len(in.Updated) > 0 { 233 | u.pb.Updated = in.Updated 234 | } 235 | if len(in.UpdatedBy) > 0 { 236 | u.pb.UpdatedBy = in.UpdatedBy 237 | } 238 | } 239 | ``` 240 | 241 | * Sekarang repository sudah bisa melakukan call grpc ke auth service kapan pun membutuhkan, misal dengan kode berikut : 242 | 243 | ```go 244 | loginInput := auth.LoginInput{username:"waresix", password:"1234"} 245 | response, err := u.authClient.Login(ctx, &loginInput) 246 | ``` 247 | 248 | -------------------------------------------------------------------------------- /design-pattern/single-flight.md: -------------------------------------------------------------------------------- 1 | # Single Flight Pattern 2 | 3 | Single Flight adalah pola yang digunakan untuk mencegah redundant request dengan memastikan bahwa hanya satu goroutine yang menjalankan proses tertentu dalam satu waktu. Goroutine lain yang meminta hasil yang sama akan menunggu hasil dari goroutine pertama, bukan memproses ulang permintaan yang sama. 4 | 5 | Pola ini sangat berguna untuk: 6 | 7 | - Mengurangi load ke database atau API eksternal (misal: caching atau fetching data). 8 | - Menghindari spam request ke third-party API, sehingga lebih efisien. 9 | - Menghindari race condition saat banyak goroutine meminta data yang sama. 10 | - Meningkatkan efisiensi dalam sistem dengan banyak request paralel. 11 | 12 | ## Kapan Teknik Ini Berguna? 13 | 14 | - Ketika banyak request ke API yang sama dalam waktu bersamaan. 15 | - Jika API third-party memiliki rate limit dan kita ingin menghindari throttling. 16 | - Untuk mengurangi latensi dengan menghindari redundant request. 17 | - Untuk menghemat biaya jika API third-party menggunakan sistem berbayar per request. 18 | 19 | ## Cara Menentukan Unique Key 20 | Seperti yang kita lihat dari contoh kode implmentasi single flight patter, ada satu unique-key yang digunakan, sehingga request-request yang memiliki unique key yang sama, hanya akan diproses 1x. Unique-key bisa digenerate dengan berbagai logic, untuk kasus pemanggilan api third party, unique-key bisa menggunakan path url termasuk dengan parameter/query yang digunakan. Jika url, path dan parameter dirasa terlalu panjang, bisa menggunakan hashing agar lebih ringkas. 21 | 22 | ```go 23 | key := fmt.Sprintf("%x", sha256.Sum256([]byte(urlWithParams))) 24 | ``` 25 | 26 | ## Implementasi Single Flight Pattern 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "crypto/sha256" 33 | "encoding/hex" 34 | "fmt" 35 | "io" 36 | "net/http" 37 | "sync" 38 | "time" 39 | ) 40 | 41 | // MySingleFlight adalah struktur untuk menangani request tunggal per key 42 | type MySingleFlight struct { 43 | mu sync.Mutex 44 | calls map[string]*call 45 | } 46 | 47 | // call menyimpan informasi tentang request yang sedang berlangsung 48 | type call struct { 49 | wg sync.WaitGroup 50 | res string 51 | err error 52 | } 53 | 54 | // NewMySingleFlight membuat instance MySingleFlight 55 | func NewMySingleFlight() *MySingleFlight { 56 | return &MySingleFlight{ 57 | calls: make(map[string]*call), 58 | } 59 | } 60 | 61 | // Do memastikan hanya satu request per key yang berjalan pada satu waktu 62 | func (sf *MySingleFlight) Do(key string, fn func() (string, error)) (string, error) { 63 | sf.mu.Lock() 64 | if c, found := sf.calls[key]; found { 65 | sf.mu.Unlock() 66 | c.wg.Wait() // Tunggu hasil request yang sedang berjalan 67 | return c.res, c.err 68 | } 69 | 70 | // Jika belum ada request, buat yang baru 71 | c := &call{} 72 | c.wg.Add(1) 73 | sf.calls[key] = c 74 | sf.mu.Unlock() 75 | 76 | // Jalankan request 77 | c.res, c.err = fn() 78 | c.wg.Done() 79 | 80 | // Hapus dari map setelah selesai 81 | sf.mu.Lock() 82 | delete(sf.calls, key) 83 | sf.mu.Unlock() 84 | 85 | return c.res, c.err 86 | } 87 | 88 | // Hash URL dengan SHA-256 sebagai key 89 | func hashURL(url string) string { 90 | hash := sha256.Sum256([]byte(url)) 91 | return hex.EncodeToString(hash[:]) 92 | } 93 | 94 | // Fetch API menggunakan MySingleFlight 95 | func fetchAPI(sf *MySingleFlight, url string) (string, error) { 96 | key := hashURL(url) 97 | 98 | return sf.Do(key, func() (string, error) { 99 | fmt.Println("Fetching API:", url) // Indikasi request benar-benar terjadi 100 | resp, err := http.Get(url) 101 | if err != nil { 102 | return "", err 103 | } 104 | defer resp.Body.Close() 105 | 106 | body, err := io.ReadAll(resp.Body) 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | return string(body), nil 112 | }) 113 | } 114 | 115 | func main() { 116 | url := "https://example.com/todos/1" // API contoh 117 | sf := NewMySingleFlight() 118 | 119 | var wg sync.WaitGroup 120 | numRequests := 3 121 | 122 | wg.Add(numRequests) 123 | for i := 0; i < numRequests; i++ { 124 | go func(id int) { 125 | defer wg.Done() 126 | data, err := fetchAPI(sf, url) 127 | if err != nil { 128 | fmt.Printf("Goroutine %d error: %v\n", id, err) 129 | } else { 130 | fmt.Printf("Goroutine %d result: %s\n", id, data) 131 | } 132 | }(i) 133 | } 134 | 135 | wg.Wait() 136 | } 137 | ``` 138 | 139 | Penjelasan Kode : 140 | 141 | - Membuat MySingleFlight 142 | 143 | - Menggunakan map[string]*call untuk menyimpan request yang sedang berjalan. 144 | - Menggunakan sync.Mutex agar hanya satu goroutine yang bisa memodifikasi map pada satu waktu. 145 | - Request kedua dan seterusnya akan menunggu hasil request pertama. 146 | 147 | - Struktur call 148 | - wg sync.WaitGroup: Menunggu hasil request yang sedang berlangsung. 149 | - res string: Menyimpan hasil response. 150 | - err error: Menyimpan error jika terjadi. 151 | 152 | - Mekanisme Do() 153 | - Jika request dengan key tertentu sudah berjalan, goroutine menunggu hasilnya (c.wg.Wait()). 154 | - belum ada request, membuat request baru dan menyimpannya di map. 155 | - Setelah request selesai, hapus entri dari map agar request baru bisa dilakukan. 156 | 157 | - Memanggil API dengan Hashing 158 | - Menggunakan SHA-256 hash dari URL sebagai key untuk menghindari duplikasi request. 159 | 160 | - Menjalankan fetchAPI() dengan Beberapa Goroutine 161 | - Tiga goroutine menjalankan request bersamaan. 162 | - Hanya satu request yang benar-benar dikirim, sisanya menunggu hasilnya. 163 | 164 | ## Output yang diharapkan 165 | 166 | ``` 167 | Fetching API: https://example.com/todos/1 168 | Goroutine 0 result: {"userId":1,"id":1,"title":"lorem ipsum delectus aut autem","completed":false} 169 | Goroutine 1 result: {"userId":1,"id":1,"title":"lorem ipsum delectus aut autem","completed":false} 170 | Goroutine 2 result: {"userId":1,"id":1,"title":"lorem ipsum delectus aut autem","completed":false} 171 | ``` 172 | 173 | - Fetching API: hanya muncul sekali, menandakan hanya satu request yang benar-benar dikirim. 174 | - Semua goroutine mendapatkan hasil yang sama tanpa harus request ulang. 175 | 176 | ## Implementasi Menggunakan Library "golang.org/x/sync/singleflight" 177 | 178 | Saat ini, sudah ada library single-flight pattern yang cukup populer di golang. Pertimbangkan untuk menggunakan library ini agar kita tidak perlu membuatnya from scratch. 179 | 180 | ```go 181 | package main 182 | 183 | import ( 184 | "crypto/sha256" 185 | "encoding/hex" 186 | "fmt" 187 | "io" 188 | "net/http" 189 | "sync" 190 | "time" 191 | 192 | "golang.org/x/sync/singleflight" 193 | ) 194 | 195 | var sf singleflight.Group 196 | 197 | // Hash URL dengan SHA-256 untuk digunakan sebagai key 198 | func hashURL(url string) string { 199 | hash := sha256.Sum256([]byte(url)) 200 | return hex.EncodeToString(hash[:]) 201 | } 202 | 203 | // Fetch data dari API third-party menggunakan SingleFlight 204 | func fetchAPI(url string) (string, error) { 205 | key := hashURL(url) // Gunakan hash URL sebagai key 206 | 207 | // Gunakan SingleFlight untuk mencegah duplikasi request 208 | result, err, _ := sf.Do(key, func() (interface{}, error) { 209 | fmt.Println("Fetching API:", url) // Indikasi request benar-benar terjadi 210 | resp, err := http.Get(url) 211 | if err != nil { 212 | return "", err 213 | } 214 | defer resp.Body.Close() 215 | 216 | body, err := io.ReadAll(resp.Body) 217 | if err != nil { 218 | return "", err 219 | } 220 | 221 | return string(body), nil 222 | }) 223 | 224 | if err != nil { 225 | return "", err 226 | } 227 | return result.(string), nil 228 | } 229 | 230 | func main() { 231 | url := "https://example.com/todos/1" // API contoh 232 | 233 | var wg sync.WaitGroup 234 | numRequests := 3 235 | 236 | wg.Add(numRequests) 237 | for i := 0; i < numRequests; i++ { 238 | go func(id int) { 239 | defer wg.Done() 240 | data, err := fetchAPI(url) 241 | if err != nil { 242 | fmt.Printf("Goroutine %d error: %v\n", id, err) 243 | } else { 244 | fmt.Printf("Goroutine %d result: %s\n", id, data) 245 | } 246 | }(i) 247 | } 248 | 249 | wg.Wait() 250 | } 251 | ``` 252 | 253 | ## Kesimpulan 254 | 255 | SingleFlight adalah solusi yang efisien dan sederhana untuk menghindari eksekusi berulang dari tugas yang sama dalam lingkungan konkuren. Jika ingin kontrol penuh, kita bisa membuat SingleFlight buatan sendiri. Jika ingin implementasi cepat dan stabil, cukup gunakan sync/singleflight. 256 | 257 | 🚀 Dengan menggunakan SingleFlight, kita bisa meningkatkan performa aplikasi secara signifikan dan menghindari pemborosan resource! -------------------------------------------------------------------------------- /grpc-framework/grpc-routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | * saat ini semua kode ada dalam 1 file server.go 4 | * pecah kode driverHandler dalam file domain/ddrivers/handler.go 5 | * karena akan kita export, pastikan type dan fungsi dubah dengan awalan huruf besar 6 | 7 | ```go 8 | package ddrivers 9 | 10 | import ( 11 | "context" 12 | "database/sql" 13 | "fmt" 14 | "log" 15 | "skeleton/pb/drivers" 16 | "skeleton/pb/generic" 17 | "strconv" 18 | "strings" 19 | "time" 20 | 21 | "github.com/google/uuid" 22 | "google.golang.org/grpc/codes" 23 | "google.golang.org/grpc/status" 24 | ) 25 | 26 | type DriverHandler struct { 27 | log *log.Logger 28 | db *sql.DB 29 | } 30 | 31 | func NewDriverHandler(log *log.Logger, db *sql.DB) *DriverHandler { 32 | handler := new(DriverHandler) 33 | handler.log = log 34 | handler.db = db 35 | return handler 36 | } 37 | 38 | func (u *DriverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 39 | out := &drivers.Drivers{} 40 | query := `SELECT id, name, phone, licence_number, company_id, company_name FROM drivers` 41 | where := []string{"is_deleted = false"} 42 | paramQueries := []interface{}{} 43 | 44 | if len(in.Ids) > 0 { 45 | orWhere := []string{} 46 | for _, id := range in.Ids { 47 | paramQueries = append(paramQueries, id) 48 | orWhere = append(orWhere, fmt.Sprintf("id = %d", len(paramQueries))) 49 | } 50 | if len(orWhere) > 0 { 51 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 52 | } 53 | } 54 | 55 | if len(in.CompanyIds) > 0 { 56 | orWhere := []string{} 57 | for _, id := range in.CompanyIds { 58 | paramQueries = append(paramQueries, id) 59 | orWhere = append(orWhere, fmt.Sprintf("company_id = %d", len(paramQueries))) 60 | } 61 | if len(orWhere) > 0 { 62 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 63 | } 64 | } 65 | 66 | if len(in.LicenceNumbers) > 0 { 67 | orWhere := []string{} 68 | for _, licenceNumber := range in.LicenceNumbers { 69 | paramQueries = append(paramQueries, licenceNumber) 70 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 71 | } 72 | if len(orWhere) > 0 { 73 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 74 | } 75 | } 76 | 77 | if len(in.Names) > 0 { 78 | orWhere := []string{} 79 | for _, name := range in.Names { 80 | paramQueries = append(paramQueries, name) 81 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 82 | } 83 | if len(orWhere) > 0 { 84 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 85 | } 86 | } 87 | 88 | if len(in.Phones) > 0 { 89 | orWhere := []string{} 90 | for _, phone := range in.Phones { 91 | paramQueries = append(paramQueries, phone) 92 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 93 | } 94 | if len(orWhere) > 0 { 95 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 96 | } 97 | } 98 | 99 | if in.Pagination == nil { 100 | in.Pagination = &generic.Pagination{} 101 | } 102 | 103 | if len(in.Pagination.Keyword) > 0 { 104 | orWhere := []string{} 105 | 106 | paramQueries = append(paramQueries, in.Pagination.Keyword) 107 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 108 | 109 | paramQueries = append(paramQueries, in.Pagination.Keyword) 110 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 111 | 112 | paramQueries = append(paramQueries, in.Pagination.Keyword) 113 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 114 | 115 | paramQueries = append(paramQueries, in.Pagination.Keyword) 116 | orWhere = append(orWhere, fmt.Sprintf("company_name = %d", len(paramQueries))) 117 | 118 | if len(orWhere) > 0 { 119 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 120 | } 121 | } 122 | 123 | if len(in.Pagination.Sort) > 0 { 124 | in.Pagination.Sort = strings.ToLower(in.Pagination.Sort) 125 | if in.Pagination.Sort != "asc" { 126 | in.Pagination.Sort = "desc" 127 | } 128 | } else { 129 | in.Pagination.Sort = "desc" 130 | } 131 | 132 | if len(in.Pagination.Order) > 0 { 133 | in.Pagination.Order = strings.ToLower(in.Pagination.Order) 134 | if !(in.Pagination.Order == "id" || 135 | in.Pagination.Order == "name" || 136 | in.Pagination.Order == "phone" || 137 | in.Pagination.Order == "licence_number" || 138 | in.Pagination.Order == "company_id" || 139 | in.Pagination.Order == "company_name") { 140 | in.Pagination.Order = "id" 141 | } 142 | } else { 143 | in.Pagination.Order = "id" 144 | } 145 | 146 | if in.Pagination.Limit <= 0 { 147 | in.Pagination.Limit = 10 148 | } 149 | 150 | if in.Pagination.Offset <= 0 { 151 | in.Pagination.Offset = 0 152 | } 153 | 154 | if len(where) > 0 { 155 | query += " WHERE " + strings.Join(where, " AND ") 156 | } 157 | 158 | query += " ORDER BY " + in.Pagination.Order + " " + in.Pagination.Sort 159 | query += " LIMIT " + strconv.Itoa(int(in.Pagination.Limit)) 160 | query += " OFFSET " + strconv.Itoa(int(in.Pagination.Offset)) 161 | 162 | rows, err := u.db.QueryContext(ctx, query, paramQueries...) 163 | if err != nil { 164 | return out, logError(u.log, codes.Internal, err) 165 | } 166 | defer rows.Close() 167 | 168 | for rows.Next() { 169 | var obj drivers.Driver 170 | err = rows.Scan(&obj.Id, &obj.Name, &obj.Phone, &obj.LicenceNumber, &obj.CompanyId, &obj.CompanyName) 171 | if err != nil { 172 | return out, logError(u.log, codes.Internal, err) 173 | } 174 | 175 | out.Driver = append(out.Driver, &obj) 176 | } 177 | 178 | if rows.Err() != nil { 179 | return out, logError(u.log, codes.Internal, rows.Err()) 180 | } 181 | 182 | return out, nil 183 | } 184 | 185 | func (u *DriverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 186 | query := ` 187 | INSERT INTO drivers ( 188 | id, name, phone, licence_number, company_id, company_name, created, created_by, updated, updated_by) 189 | VALUES ($1, $2, $3 ,$4, $5, $6, $7, $8, $9, $10) 190 | ` 191 | in.Id = uuid.New().String() 192 | now := time.Now().Format("2006-01-02 15:04:05.000000") 193 | _, err := u.db.ExecContext(ctx, query, 194 | in.Id, in.Name, in.Phone, in.LicenceNumber, in.CompanyId, in.CompanyName, now, "jaka", now, "jaka") 195 | 196 | if err != nil { 197 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 198 | } 199 | 200 | return in, nil 201 | } 202 | 203 | func (u *DriverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 204 | query := ` 205 | UPDATE drivers 206 | SET name = $1, 207 | phone = $2, 208 | licence_number = $3, 209 | updated = $4, 210 | updated_by = $5 211 | WHERE id = $6 212 | ` 213 | now := time.Now().Format("2006-01-02 15:04:05.000000") 214 | _, err := u.db.ExecContext(ctx, query, 215 | in.Name, in.Phone, in.LicenceNumber, now, "jaka", in.Id) 216 | 217 | if err != nil { 218 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 219 | } 220 | 221 | return in, nil 222 | } 223 | 224 | func (u *DriverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 225 | query := ` 226 | UPDATE drivers 227 | SET is_deleted = true 228 | WHERE id = $1 229 | ` 230 | _, err := u.db.ExecContext(ctx, query, in.Id) 231 | 232 | if err != nil { 233 | return &generic.BoolMessage{IsTrue: false}, logError(u.log, codes.Internal, err) 234 | } 235 | 236 | return &generic.BoolMessage{IsTrue: true}, nil 237 | } 238 | 239 | func logError(log *log.Logger, code codes.Code, err error) error { 240 | log.Print(err.Error()) 241 | return status.Error(code, err.Error()) 242 | } 243 | ``` 244 | 245 | * pecah kode routing dalam file route/route.go 246 | 247 | ```go 248 | package route 249 | 250 | import ( 251 | "database/sql" 252 | "log" 253 | "skeleton/domain/ddrivers" 254 | "skeleton/pb/drivers" 255 | 256 | "google.golang.org/grpc" 257 | ) 258 | 259 | func GrpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB) { 260 | driverServer := ddrivers.NewDriverHandler(log, db) 261 | 262 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 263 | } 264 | ``` 265 | 266 | * Ubah file server.go 267 | 268 | ```go 269 | package main 270 | 271 | import ( 272 | "log" 273 | "net" 274 | "os" 275 | 276 | "skeleton/config" 277 | "skeleton/lib/database/postgres" 278 | "skeleton/route" 279 | 280 | _ "github.com/lib/pq" 281 | "google.golang.org/grpc" 282 | ) 283 | 284 | func main() { 285 | config.Setup(".env") 286 | 287 | log := log.New(os.Stdout, "Skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 288 | 289 | db, err := postgres.Open() 290 | if err != nil { 291 | log.Fatalf("connecting to db: %v", err) 292 | return 293 | } 294 | log.Print("connecting to postgresql database") 295 | 296 | defer db.Close() 297 | 298 | // listen tcp port 299 | lis, err := net.Listen("tcp", ":"+os.Getenv("PORT")) 300 | if err != nil { 301 | log.Fatalf("failed to listen: %v", err) 302 | return 303 | } 304 | 305 | grpcServer := grpc.NewServer() 306 | 307 | // routing grpc services 308 | route.GrpcRoute(grpcServer, log, db) 309 | 310 | if err := grpcServer.Serve(lis); err != nil { 311 | log.Fatalf("failed to serve: %s", err) 312 | return 313 | } 314 | log.Print("serve grpc on port: " + os.Getenv("PORT")) 315 | 316 | } 317 | ``` 318 | 319 | -------------------------------------------------------------------------------- /golang-fundamental/pseudo_oop.md: -------------------------------------------------------------------------------- 1 | # Pseudo OOP 2 | 3 | Golang bukan merupakan bahasa pemrograman yang berorientasi objek. Tapi golang memiliki fitur seperti type, struct, method, reference dan interface yang memungkinkan untuk melakukan pemrograman yang mirip dengan OOP. 4 | 5 | ## struct 6 | 7 | * sebuah tipe data abstract 8 | * berisi dari kumpulan dari berbagai type 9 | * struct bisa digunakan dalam konsep class 10 | 11 | ```go 12 | type User struct { 13 | ID uint64 14 | Name string 15 | } 16 | 17 | func main() { 18 | var user User 19 | user.ID = 1 20 | user.Name = "Jacky" 21 | fmt.Printf("%v\n", user) 22 | println(user.Name) 23 | 24 | user2 := User{ID: 2, Name: "JetLee"} 25 | fmt.Printf("%v\n", user2) 26 | println(user2.Name) 27 | } 28 | ``` 29 | 30 | ## Method 31 | 32 | * Kita bisa mendefiniskan suatu method pada sebuah type. 33 | * Method adalah fungsi yang mempunyai argumen khusus receiver berupa type. 34 | 35 | ```go 36 | package main 37 | 38 | type MyStr string 39 | 40 | func (m MyStr) Salam() { 41 | m = "Selamat Pagi" 42 | println(m) 43 | } 44 | 45 | func main() { 46 | var str MyStr 47 | str.Salam() 48 | } 49 | ``` 50 | 51 | * Type yang bisa dibuatkan method adalah type local, yaitu type yang ada dalam paket yang sama dengan method yang dibuat. 52 | 53 | ```go 54 | package main 55 | 56 | // ini error karena string bukan type local dalam paket main 57 | func (m string) Salam() { 58 | m = "Selamat Pagi" 59 | println(m) 60 | } 61 | 62 | func main() { 63 | var str string 64 | str.Salam() 65 | } 66 | ``` 67 | 68 | * Receiver bisa berupa pointer 69 | 70 | ```go 71 | package main 72 | 73 | type myStr string 74 | 75 | func (m *myStr) Change() { 76 | *m = myStr("Selamat Sore") 77 | } 78 | 79 | func (m *myStr) Print() { 80 | println(*m) 81 | } 82 | 83 | func main() { 84 | str := myStr("Selamat Pagi") 85 | str.Print() 86 | str.Change() 87 | str.Print() 88 | } 89 | ``` 90 | 91 | ## Interface 92 | 93 | * Interface berisi kumpulan yang berisi method yang abstract 94 | 95 | ```go 96 | type i interface{ 97 | method() 98 | } 99 | ``` 100 | 101 | * Type lain akan mengimplementasikan method dalam interface 102 | * Tidak ada perintah implement, suatu interface akan dipenuhi secara implisit begitu ada yang mengimplementasikannya 103 | 104 | ```go 105 | package main 106 | 107 | type i interface { 108 | method() 109 | } 110 | 111 | type myStr string 112 | 113 | func (m *myStr) method() { 114 | println(*m) 115 | } 116 | 117 | func main() { 118 | var i i 119 | str := myStr("Hello") 120 | i = &str 121 | i.method() 122 | } 123 | ``` 124 | 125 | * Jika suatu interface diinisiasi tapi tidak ada yang mengimplementasikannya akan terjadi error nil pointer dereference 126 | 127 | ```go 128 | package main 129 | 130 | type i interface { 131 | method() 132 | } 133 | 134 | func main() { 135 | var i i 136 | i.method() 137 | } 138 | ``` 139 | 140 | * Isi interface dapat dibayangkan sebagai sebuah pasangan nilai dan sebuah tipe: `(nilai, type)` 141 | 142 | ```go 143 | package main 144 | 145 | type i interface{ 146 | method() 147 | } 148 | 149 | type myStr string 150 | 151 | func (m *myStr) method() { 152 | println(*m) 153 | } 154 | 155 | func main() { 156 | var i i 157 | str := myStr("Hello") 158 | i = &str 159 | i.method() 160 | describe(i) 161 | } 162 | 163 | func describe(i I) { 164 | fmt.Printf("(%v, %T)\n", i, i) 165 | } 166 | ``` 167 | 168 | ## Interface Kosong 169 | 170 | * Interface kosong merupakan interface yang tidak memiliki method 171 | * Untuk mengklaim nilai interface harus dilakukan type asserting 172 | 173 | ```go 174 | var a interface{} 175 | a = "string" 176 | println(a.(string)) 177 | 178 | a = false 179 | println(a.(bool)) 180 | if value, ok := a.(bool); ok { 181 | println(value) 182 | } 183 | 184 | myMap := map[string]interface{}{"Satu": true, "Dua": "string", "Tiga": uint(3)} 185 | println(myMap["Satu"].(bool)) 186 | println(myMap["Dua"].(string)) 187 | println(myMap["Tiga"].(uint)) 188 | ``` 189 | 190 | * Penggunaan switch type dalam melakukan asserting 191 | 192 | ```go 193 | package main 194 | 195 | type myStr string 196 | 197 | func main() { 198 | var a interface{} 199 | a = myStr("Jacky") 200 | 201 | switch t := a.(type) { 202 | case string : 203 | println("type string", t) 204 | case bool : 205 | println("type bool", t) 206 | case myStr : 207 | println("type myStr", t) 208 | default : 209 | println("type lainnya", t) 210 | } 211 | } 212 | ``` 213 | 214 | ## Pseudo Object 215 | 216 | * tidak ada class dalam go, tapi kita bisa menggunakan type 217 | * variable class diganti dengan type struct 218 | * method class diganti dengan method dengan pointer reference 219 | * gunakan kata kunci new\(\) untuk membuat object 220 | 221 | ```go 222 | package main 223 | 224 | import ( 225 | "fmt" 226 | ) 227 | 228 | type becak struct { 229 | roda int 230 | warna string 231 | } 232 | 233 | func (o *becak) caraJalan() string { 234 | return "dikayuh" 235 | } 236 | 237 | func main() { 238 | becak1 := becak{roda: 3, warna: "biru"} 239 | fmt.Printf("%v, %T\n", becak1, becak1) 240 | println("cara jalan:", becak1.caraJalan()) 241 | 242 | becak2 := &becak1 243 | fmt.Printf("%v, %T\n", becak2, becak2) 244 | println("cara jalan:", becak2.caraJalan()) 245 | 246 | becak3 := new(becak) 247 | becak3.roda = 3 248 | becak3.warna = "merah" 249 | fmt.Printf("%v, %T\n", becak3, becak3) 250 | println("cara jalan:", becak3.caraJalan()) 251 | } 252 | ``` 253 | 254 | ### Method Overloading 255 | 256 | * Method overloading dimungkinkan dengan reference yang berbeda 257 | 258 | ```go 259 | package main 260 | 261 | import ( 262 | "fmt" 263 | ) 264 | 265 | type becak struct { 266 | roda int 267 | warna string 268 | } 269 | 270 | type gerobak struct { 271 | roda int 272 | warna string 273 | } 274 | 275 | func (o *becak) caraJalan() string { 276 | return "dikayuh" 277 | } 278 | 279 | func (o *gerobak) caraJalan() string { 280 | return "didorong" 281 | } 282 | 283 | func main() { 284 | becak := new(becak) 285 | println("becak", "cara jalan:", becak.caraJalan()) 286 | 287 | gerobak := new(gerobak) 288 | println("gerobak", "cara jalan:", gerobak.caraJalan()) 289 | } 290 | ``` 291 | 292 | ### Encapsulation 293 | 294 | * Encapsulasi terjadi di level paket. 295 | * Kita bisa memilih kode \(type, variabel, fungsi dll\) yang hendak diexport ke luar paket dan mana yang hanya bisa diakses dalam paket yang sama. 296 | * Penamaan kode yang bersifat publik diawali dengan huruf besar. 297 | * Penamaan kode yang bersifat privat diawali dengan huruf kecil. 298 | 299 | ```go 300 | // file APP/latihan/kendaraan.go 301 | package latihan 302 | 303 | // Kendaraan interface 304 | type Kendaraan interface { 305 | CaraJalan() string 306 | SetWarna(string) 307 | GetWarna() string 308 | GetRoda() int 309 | } 310 | 311 | type becak struct { 312 | roda int 313 | warna string 314 | } 315 | 316 | func (o *becak) SetWarna(s string) { 317 | o.warna = s 318 | } 319 | 320 | func (o *becak) GetWarna() string { 321 | return o.warna 322 | } 323 | 324 | func (o *becak) GetRoda() int { 325 | return 3 326 | } 327 | 328 | func (o *becak) CaraJalan() string { 329 | return "dikayuh" 330 | } 331 | 332 | // NewBecak function untuk membuat objek becak 333 | func NewBecak() Kendaraan { 334 | return &becak{} 335 | } 336 | ``` 337 | 338 | ```go 339 | package main 340 | 341 | import ( 342 | "golang-essentials/latihan" 343 | ) 344 | 345 | func main() { 346 | becak := latihan.NewBecak() 347 | becak.SetWarna("Biru") 348 | println(becak.CaraJalan()) 349 | println("jumlah roda:", becak.GetRoda()) 350 | println("warna:", becak.GetWarna()) 351 | } 352 | ``` 353 | 354 | ### Inheritance 355 | 356 | * Go memungkinkan inheritance melalui embedded berupa field anonim 357 | 358 | ```go 359 | package main 360 | 361 | import ( 362 | "fmt" 363 | ) 364 | 365 | type User struct { 366 | Name string 367 | Gender string 368 | Address 369 | } 370 | 371 | type Address struct { 372 | Street string 373 | Number string 374 | City string 375 | Zipcode string 376 | } 377 | 378 | func main () { 379 | user := new(User) 380 | user.Name = "Wiro" 381 | user.Gender = "Male" 382 | user.Street = "Marlioboro" 383 | user.Number = "212" 384 | user.City = "Jogja" 385 | 386 | fmt.Printf("%v", user) 387 | } 388 | ``` 389 | 390 | * Tapi banyak programmer golang yang tidak menyarankan untuk melakukan inheritance. Melainkan melakukan pendekatan object composition. 391 | 392 | ### Object Composition 393 | 394 | * Daripada melakukan pseudo inheritance melalui embedded, disarankan untuk melakukan object composition 395 | 396 | ```go 397 | package main 398 | 399 | import ( 400 | "fmt" 401 | ) 402 | 403 | type User struct { 404 | Name string 405 | Gender string 406 | Address Address 407 | } 408 | 409 | type Address struct { 410 | Street string 411 | Number string 412 | City string 413 | Zipcode string 414 | } 415 | 416 | func main () { 417 | user := new(User) 418 | user.Name = "Wiro" 419 | user.Gender = "Male" 420 | user.Address.Street = "Marlioboro" 421 | user.Address.Number = "212" 422 | user.Address.City = "Jogja" 423 | 424 | fmt.Printf("%v", user) 425 | } 426 | ``` 427 | 428 | ### Polymorphism 429 | 430 | ```go 431 | package main 432 | 433 | import "fmt" 434 | 435 | type Hewan struct { 436 | Nama string 437 | Nyata bool 438 | } 439 | 440 | func (c *Hewan) Cetak() { 441 | fmt.Printf("Nama: '%s', Nyata: %t\n", c.Nama, c.Nyata) 442 | } 443 | 444 | type HewanTerbang struct { 445 | Hewan 446 | PanjangSayap int 447 | } 448 | 449 | func (c HewanTerbang) Cetak() { 450 | fmt.Printf("Nama: '%s', Nyata: %t, PanjangSayap: %d\n", c.Nama, c.Nyata, c.PanjangSayap) 451 | } 452 | 453 | type Unicorn struct { 454 | Hewan 455 | } 456 | 457 | type Naga struct { 458 | HewanTerbang 459 | } 460 | 461 | type Pterodactilus struct { 462 | HewanTerbang 463 | } 464 | 465 | func NewPterodactyl(panjangSayap int) *Pterodactilus { 466 | p := new(Pterodactilus) 467 | p.Nama = "Pterodactilus" 468 | p.Nyata = true 469 | p.PanjangSayap = panjangSayap 470 | 471 | return p 472 | } 473 | 474 | func main() { 475 | hewan := new(Hewan) 476 | hewan.Nama = "Sembarang hewan" 477 | hewan.Nyata = false 478 | 479 | naga := new(Naga) 480 | naga.Nama = "Naga" 481 | naga.Nyata = false 482 | 483 | uni := new(Unicorn) 484 | uni.Nama = "Unicorn" 485 | uni.Nyata = false 486 | 487 | p1 := new(Pterodactilus) 488 | p1.Nama = "Pterodactilus" 489 | p1.Nyata = true 490 | p1.PanjangSayap = 5 491 | 492 | p2 := NewPterodactyl(8) 493 | 494 | hewan.Cetak() 495 | naga.Cetak() 496 | uni.Cetak() 497 | p1.Cetak() 498 | p2.Cetak() 499 | 500 | animals := []*Hewan{ 501 | hewan, 502 | &naga.Hewan, 503 | &uni.Hewan, 504 | &p1.Hewan, 505 | &p2.Hewan, 506 | } 507 | fmt.Println("Cetak() melalui embedded type Hewan") 508 | for _, c := range animals { 509 | c.Cetak() 510 | } 511 | } 512 | ``` 513 | 514 | -------------------------------------------------------------------------------- /design-pattern/worker-pool.md: -------------------------------------------------------------------------------- 1 | # Worker Pool 2 | Worker Pool adalah pola konkurensi di mana sejumlah tetap goroutine (worker) dijalankan untuk menangani tugas dari antrian pekerjaan (job queue). Dengan pendekatan ini, kita dapat menghindari overhead akibat terlalu banyak goroutine yang berjalan secara bersamaan. 3 | 4 | ## Konsep Utama: 5 | - Job Queue: Tempat di mana pekerjaan ditampung sebelum diproses oleh worker. 6 | - Workers (Goroutines): Sejumlah tetap goroutine yang mengambil dan memproses pekerjaan dari job queue. 7 | - Result Channel (Opsional): Jika pekerjaannya menghasilkan output, hasilnya bisa dikirim melalui channel. 8 | 9 | ## Kapan Menggunakan Worker Pool? 10 | - Jika ada banyak tugas independen yang bisa dieksekusi secara paralel. 11 | - Jika jumlah goroutine perlu dibatasi untuk menghindari konsumsi resource berlebih. 12 | - Jika ingin meningkatkan efisiensi pemrosesan dengan menghindari overhead pembuatan goroutine yang berlebihan. 13 | 14 | ## Implementasi Worker Pool di Golang 15 | Berikut contoh implementasi Worker Pool sederhana di Golang: 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "math/rand" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | // Struktur untuk mewakili tugas (job) 28 | type Job struct { 29 | ID int 30 | } 31 | 32 | // Fungsi worker yang mengambil job dari channel dan memprosesnya 33 | func worker(id int, jobs <-chan Job, wg *sync.WaitGroup) { 34 | defer wg.Done() 35 | for job := range jobs { 36 | fmt.Printf("Worker %d memproses job %d\n", id, job.ID) 37 | time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond) // Simulasi pekerjaan 38 | } 39 | } 40 | 41 | func main() { 42 | const numWorkers = 3 // Jumlah worker 43 | const numJobs = 10 // Jumlah pekerjaan 44 | 45 | jobs := make(chan Job, numJobs) // Channel untuk menyimpan jobs 46 | var wg sync.WaitGroup 47 | 48 | // Memulai worker 49 | for i := 1; i <= numWorkers; i++ { 50 | wg.Add(1) 51 | go worker(i, jobs, &wg) 52 | } 53 | 54 | // Mengirimkan jobs ke dalam channel 55 | for j := 1; j <= numJobs; j++ { 56 | jobs <- Job{ID: j} 57 | } 58 | 59 | close(jobs) // Menutup channel jobs agar worker tahu tidak ada job baru 60 | wg.Wait() // Menunggu semua worker selesai 61 | fmt.Println("Semua pekerjaan telah selesai!") 62 | } 63 | ``` 64 | 65 | ## Penjelasan Kode: 66 | 67 | 1. Channel jobs digunakan sebagai job queue. 68 | 2. Worker (worker function) membaca dari jobs dan memproses pekerjaan. 69 | 3. Loop utama membuat numWorkers goroutine untuk worker. 70 | 4. Jobs dimasukkan ke dalam channel. 71 | 5. Channel jobs ditutup untuk memberi sinyal bahwa tidak ada job baru. 72 | 6. WaitGroup digunakan untuk menunggu semua worker menyelesaikan tugasnya. 73 | 74 | ## Keuntungan Worker Pool 75 | - Membatasi jumlah goroutine → Menghindari overhead dari terlalu banyak goroutine. 76 | - Efisiensi pemrosesan → Tugas didistribusikan ke worker secara merata. 77 | - Lebih scalable → Bisa dengan mudah menyesuaikan jumlah worker. 78 | 79 | ## Kapan Tidak Menggunakan Worker Pool? 80 | - Jika jumlah tugas kecil dan overhead goroutine tidak menjadi masalah. 81 | - Jika setiap pekerjaan membutuhkan sumber daya unik dan tidak bisa dibagikan antar worker. 82 | 83 | ## Best Practise Menentukan Jumlah Jobs dan Worker 84 | 85 | Menentukan jumlah jobs dalam Worker Pool sangat bergantung pada beberapa faktor seperti jumlah worker, kapasitas CPU, I/O, dan sifat pekerjaan itu sendiri. 86 | 87 | ### 1. Berdasarkan Jumlah Worker dan Sifat Pekerjaan 88 | 89 | ``` 90 | Jumlah Jobs ≥ Jumlah Worker 91 | ``` 92 | 93 | Mengapa? Jika jumlah jobs lebih kecil dari jumlah worker, ada worker yang idle (menganggur), yang berarti resource tidak digunakan secara optimal. 94 | 95 | Namun, jumlah jobs tidak boleh terlalu besar tanpa mempertimbangkan beban kerja karena bisa menyebabkan bottleneck. 96 | 97 | ### 2. Berdasarkan Tipe Pekerjaan (CPU-Bound vs. I/O-Bound) 98 | 99 | Pekerjaan yang dilakukan dalam worker menentukan jumlah jobs yang ideal. 100 | 101 | #### A. CPU-Bound (Butuh Banyak Perhitungan) 102 | 103 | - Contoh: Enkripsi, kompresi, machine learning inference, hashing, perhitungan matematis intensif. 104 | - Worker biasanya dibatasi oleh jumlah CPU core. 105 | - Formula Optimal: 106 | ``` 107 | Jumlah Worker ≈ Jumlah Core CPU 108 | ``` 109 | atau sedikit lebih besar untuk mengakomodasi overhead switching. 110 | Misalnya: 111 | - Jika CPU memiliki 8 core, maka worker bisa 8-12. 112 | - Jumlah jobs bisa dibuat 2x dari worker untuk memastikan ada tugas yang selalu bisa diambil worker. 113 | 114 | #### B. I/O-Bound (Sering Menunggu Respons) 115 | - Contoh: HTTP requests, database queries, file I/O, network calls. 116 | - Karena pekerjaan ini sering menunggu, jumlah worker bisa lebih besar dibanding CPU core. 117 | - Formula Optimal: 118 | ``` 119 | Jumlah Worker ≈ (Jumlah Core CPU * 2) atau lebih tinggi 120 | ``` 121 | 122 | atau 123 | 124 | ``` 125 | Jumlah Worker ≈ (Jumlah Concurrent Requests / Waktu Tunggu Rata-rata) 126 | ``` 127 | Misalnya: Jika sistem menangani banyak API call dengan waktu respons 500ms, dan ingin menangani 1000 request per detik worker bisa sekitar (1000 / 0.5) = 2000. 128 | 129 | ### 3. Benchmark & Profiling 130 | 131 | Cara terbaik menentukan jumlah jobs adalah dengan benchmarking dan profiling. 132 | Gunakan tools seperti: 133 | - pprof (Golang built-in profiler) 134 | - htop (monitor CPU usage) 135 | - wrk (untuk load testing HTTP API) 136 | - Apache JMeter (untuk uji beban) 137 | 138 | Langkah Benchmarking: 139 | - Mulai dengan jumlah worker = jumlah core CPU. 140 | - Uji performa dengan jumlah jobs yang berbeda (misal, 1x, 2x, 4x dari worker). 141 | - Pantau CPU, RAM, dan latensi untuk melihat titik optimal. 142 | - Jika worker idle lama, bisa ditambah jobs. 143 | - Jika CPU usage selalu 100% tanpa peningkatan throughput, jobs mungkin terlalu banyak. 144 | 145 | ### 4. Contoh Implementasi Adaptif 146 | Jika ingin menyesuaikan jumlah worker secara otomatis, kita bisa mendeteksi jumlah core CPU dengan runtime.NumCPU(): 147 | ```go 148 | package main 149 | 150 | import ( 151 | "fmt" 152 | "runtime" 153 | "sync" 154 | "time" 155 | ) 156 | 157 | func worker(id int, jobs <-chan int, wg *sync.WaitGroup) { 158 | defer wg.Done() 159 | for job := range jobs { 160 | fmt.Printf("Worker %d memproses job %d\n", id, job) 161 | time.Sleep(500 * time.Millisecond) // Simulasi pekerjaan 162 | } 163 | } 164 | 165 | func main() { 166 | numCPU := runtime.NumCPU() // Deteksi jumlah core CPU 167 | numWorkers := numCPU * 2 // Bisa dikalikan 2 untuk I/O-Bound 168 | numJobs := numWorkers * 2 // Jumlah jobs minimal 2x worker 169 | 170 | jobs := make(chan int, numJobs) 171 | var wg sync.WaitGroup 172 | 173 | // Memulai worker 174 | for i := 1; i <= numWorkers; i++ { 175 | wg.Add(1) 176 | go worker(i, jobs, &wg) 177 | } 178 | 179 | // Kirim jobs 180 | for j := 1; j <= numJobs; j++ { 181 | jobs <- j 182 | } 183 | 184 | close(jobs) 185 | wg.Wait() 186 | fmt.Println("Semua pekerjaan selesai!") 187 | } 188 | ``` 189 | 190 | ### Kesimpulan Best Practice 191 | 192 | - CPU-Bound → Worker ≈ Jumlah Core CPU 193 | - I/O-Bound → Worker bisa lebih banyak (Core CPU * 2 atau lebih) 194 | - Jumlah Jobs ≥ Jumlah Worker, tetapi tidak terlalu besar untuk menghindari bottleneck 195 | - Gunakan Benchmarking & Profiling untuk menentukan jumlah optimal 196 | 197 | ## Jebakan Goroutine 198 | Ya, kita sudah mengimplementasikan pattern worker pool untuk mencegah overhead, kita sudah memperkirakan jumlah worker dengan baik. Tapi bagaimana jika ada developer lain (tanpa kordinasi) membuat goroutine juga di fungsi lain? Ini mengakibatkan perhitungan jumlah worker yang kita buat menjadi tidak valid, dan berpotensi tinggi untuk mengalami overhead. Ini karena jumlah total goroutine bisa melampaui kapasitas optimal, yang dapat menyebabkan beberapa masalah seperti: 199 | 200 | 1. CPU Starvation 201 | 202 | - Jika jumlah goroutine lebih banyak dari jumlah thread OS, CPU harus sering melakukan context switching, yang bisa mengurangi performa daripada meningkatkannya. 203 | - Misalnya, jika ada 1000 goroutine aktif tetapi hanya ada 8 CPU core, maka setiap goroutine mendapat jatah waktu sangat kecil, yang bisa memperlambat eksekusi. 204 | 205 | 2. Konsumsi Memori Berlebih 206 | - Setiap goroutine membutuhkan stack memory (~2 KB awal, bisa berkembang). Jika jumlahnya terlalu banyak, RAM bisa cepat habis. 207 | 208 | 3. Deadlock & Goroutine Leaks 209 | - Jika ada goroutine yang tidak dikontrol dengan baik (misalnya, tidak membaca dari channel atau tidak diberi timeout), ini bisa menyebabkan deadlock atau memory leaks. 210 | 211 | ## Global Worker Pool 212 | Untuk memastikan setiap developer yang terlibat tidak membuat goroutine sendiri yang berpotensi membuat overhead, kita bisa mengimplementasikan global worker pool. Alih-alih membuat goroutine, developer cukup mengirimkan pekerjaan (job) ke worker pool yang sudah ada. 213 | 214 | ### Pendekatan 215 | 📌 Worker Pool sebagai Singleton 216 | - Worker pool dibuat satu kali saat aplikasi berjalan. 217 | - Developer lain cukup mengirimkan pekerjaan ke job queue, tanpa perlu membuat goroutine sendiri. 218 | 219 | 📌 Menggunakan Channel untuk Job Queue 220 | - Developer cukup mengirimkan job ke channel. 221 | - Pekerjaan akan diproses oleh worker pool yang ada. 222 | 223 | 📌 Thread-Safe dengan sync.Once 224 | - Gunakan sync.Once untuk memastikan worker pool hanya dibuat satu kali. 225 | 226 | ### Implementasi 227 | 228 | Berikut adalah contoh implementasi worker pool yang bisa digunakan oleh semua developer tanpa perlu membuat goroutine sendiri. 229 | 230 | ```go 231 | package workerpool 232 | 233 | import ( 234 | "fmt" 235 | "log" 236 | "sync" 237 | "time" 238 | ) 239 | 240 | // Job represents a task to be processed 241 | type Job struct { 242 | ID int 243 | Payload string 244 | } 245 | 246 | // WorkerPool struct 247 | type WorkerPool struct { 248 | jobQueue chan Job 249 | numWorkers int 250 | once sync.Once 251 | wg sync.WaitGroup 252 | } 253 | 254 | var pool *WorkerPool 255 | 256 | // NewWorkerPool creates a singleton worker pool 257 | func NewWorkerPool(numWorkers, jobQueueSize int) *WorkerPool { 258 | if pool == nil { 259 | pool = &WorkerPool{ 260 | jobQueue: make(chan Job, jobQueueSize), 261 | numWorkers: numWorkers, 262 | } 263 | pool.startWorkers() 264 | } 265 | return pool 266 | } 267 | 268 | // startWorkers initializes the worker pool 269 | func (wp *WorkerPool) startWorkers() { 270 | wp.once.Do(func() { 271 | log.Println("Starting worker pool with", wp.numWorkers, "workers") 272 | for i := 0; i < wp.numWorkers; i++ { 273 | wp.wg.Add(1) 274 | go wp.worker(i) 275 | } 276 | }) 277 | } 278 | 279 | // worker function processes jobs 280 | func (wp *WorkerPool) worker(workerID int) { 281 | defer wp.wg.Done() 282 | for job := range wp.jobQueue { 283 | log.Printf("Worker %d processing job: %d with payload: %s\n", workerID, job.ID, job.Payload) 284 | time.Sleep(1 * time.Second) // Simulate processing time 285 | } 286 | } 287 | 288 | // SubmitJob allows developers to add a job to the pool 289 | func (wp *WorkerPool) SubmitJob(job Job) { 290 | wp.jobQueue <- job 291 | } 292 | 293 | // Shutdown gracefully stops the worker pool 294 | func (wp *WorkerPool) Shutdown() { 295 | close(wp.jobQueue) 296 | wp.wg.Wait() 297 | log.Println("Worker pool shut down") 298 | } 299 | ``` 300 | 301 | Berikut adalah contoh cara menggunakan worker pool dalam aplikasi utama (package main). 302 | 303 | ```go 304 | package main 305 | 306 | import ( 307 | "fmt" 308 | "myapp/workerpool" 309 | "time" 310 | ) 311 | 312 | func main() { 313 | // Inisialisasi worker pool global dengan 5 workers dan queue size 10 314 | wp := workerpool.NewWorkerPool(5, 10) 315 | 316 | // Developer lain cukup memanggil SubmitJob tanpa membuat goroutine 317 | for i := 1; i <= 20; i++ { 318 | job := workerpool.Job{ 319 | ID: i, 320 | Payload: fmt.Sprintf("Job data %d", i), 321 | } 322 | wp.SubmitJob(job) 323 | } 324 | 325 | // Tunggu sebentar untuk melihat output 326 | time.Sleep(5 * time.Second) 327 | 328 | // Graceful shutdown 329 | wp.Shutdown() 330 | } 331 | ``` 332 | 333 | ### Bagaimana Ini Mengatasi Masalah Developer Lain Membuat Goroutine? 334 | 335 | ✅ Worker Pool Sudah Ada → Developer Tidak Perlu Buat Goroutine Sendiri 336 | - Developer cukup memanggil wp.SubmitJob(job) untuk menambahkan pekerjaan ke queue. 337 | - Semua pekerjaan akan diproses oleh worker pool yang ada, tanpa perlu goroutine tambahan. 338 | 339 | ✅ Job Queue Menjaga Batasan Beban 340 | - Jika developer lain mengirim terlalu banyak job, worker pool hanya akan memproses sesuai kapasitas queue. 341 | 342 | ✅ Thread-Safe dan Singleton 343 | - Worker pool dibuat sekali saja menggunakan sync.Once. 344 | - Semua developer berbagi satu worker pool global. 345 | 346 | ✅ Graceful Shutdown 347 | - Worker pool bisa dihentikan dengan aman menggunakan Shutdown(). 348 | 349 | ## Kesiumpulan 350 | 351 | 🚀 Dengan implementasi ini: 352 | - Developer tidak perlu membuat goroutine sendiri. 353 | - Semua pekerjaan akan otomatis diproses oleh worker pool. 354 | - Thread-safe dan efisien untuk menangani concurrent jobs. 355 | 356 | Ini sudah siap dipakai untuk sistem skala besar seperti gRPC handler, HTTP request handler, atau background job processing! 😃 -------------------------------------------------------------------------------- /golang-fundamental/basic.md: -------------------------------------------------------------------------------- 1 | # Basic Golang 2 | 3 | Bab ini membahas dasar-dasar pemrograman golang 4 | 5 | ## Installasi 6 | 7 | Anda dapat membaca [dokumentasi instalasi golang](https://golang.org/doc/install) 8 | 9 | ## Membuat Projek Baru 10 | 11 | * Buat folder baru untuk memulai project 12 | * go mod init golang-latihan 13 | * Buat file main.go yang berisi kode berikut 14 | * Jalankan `go run main.go` 15 | 16 | ```go 17 | package main 18 | 19 | func main(){ 20 | println("Hello World") 21 | } 22 | ``` 23 | 24 | ## Fundamental 25 | 26 | ### Package 27 | 28 | * Setiap program GO terdiri paket-paket. 29 | * Program mulai berjalan dari paket utama \(main package\). 30 | * Dalam satu project hanya boleh ada satu package main 31 | * Selain paket main, nama paket harus sama dengan nama folder. 32 | 33 | ### Type 34 | 35 | * Go is a statically typed programming language 36 | * Setiap variabel, konstanta dan lain-lain memiliki type 37 | * tipe dasar : 38 | 39 | ```text 40 | bool 41 | 42 | string 43 | 44 | int int8 int16 int32 int64 45 | uint uint8 uint16 uint32 uint64 uintptr 46 | 47 | byte // alias untuk uint8 48 | 49 | rune // alias untuk int32 50 | // merepresentasikan sebuah kode Unicode 51 | 52 | float32 float64 53 | 54 | complex64 complex128 55 | ``` 56 | 57 | * Ada tipe turunan seperti array, slice, map, interface, struct, dan function 58 | 59 | ### Konstanta 60 | 61 | ```go 62 | const pi float64 = 22/7 63 | const ( 64 | alamat string = "Gandaria City, Jakarta Selatan" 65 | email string = "emailku@waresix.com" 66 | ) 67 | ``` 68 | 69 | ### Variabel 70 | 71 | ```go 72 | package repositories 73 | // contoh variabel yang siklus hidupnya ada dalam satu paket. 74 | // Seluruh kode dalam paket repositories, biarpun berbeda file bisa mengakses variabel ini 75 | var err error 76 | 77 | func Satu() { 78 | // NOTE : siklus hidup variabel-variable ini hanya berlaku dalam fungsi Satu 79 | 80 | // variabel harus dideklarasikan terlebih dahulu 81 | var a int 82 | 83 | // variable yang telah dideklarasikan bisa diisi dengan nilai yang sesuai 84 | a = 1 85 | 86 | // variabel bisa dideklarasikan secara implisit dan sekaligus langsung diberi nilai. 87 | // tipe akan disematkan secara implisit pada variabel ini 88 | b := 2 89 | 90 | // siklus hidup variabel bisa hanya dalam blok yang membatasi. 91 | // blok bisa berupa blok if, for, fungsi atau bahkan hanya notasi blok saja. 92 | { 93 | var c string 94 | c = "variabel di dalam blok" 95 | println(c) 96 | } 97 | } 98 | ``` 99 | 100 | ### Siklus hidup variabel 101 | 102 | ```go 103 | package repositories 104 | // contoh variabel yang siklus hidupnya ada dalam satu paket. 105 | // Seluruh kode dalam paket repositories, biarpun berbeda file bisa mengakses variabel ini 106 | var err error 107 | 108 | func Satu() { 109 | // siklus hidup variabel-variable ini hanya berlaku dalam fungsi Satu 110 | 111 | var a int 112 | a = 1 113 | 114 | b := 2 115 | 116 | // siklus hidup variabel bisa hanya dalam blok yang membatasi. 117 | // blok bisa berupa blok if, for, fungsi atau bahkan hanya notasi blok saja. 118 | { 119 | var c string 120 | c = "variabel di dalam blok" 121 | println(c) 122 | } 123 | } 124 | ``` 125 | 126 | ### Pengoptimalan penggunaan memori dalam siklus hidup variabel 127 | 128 | * Jika ingin membuat variabel global dalam satu paket, sebaiknya pertimbangkan kembali, karena siklus hidupnya ada di seluruh kode dalam paket tersebut 129 | * Untuk menghemat memori, deklarasikan variabel sesuai dengan kebutuhan siklus hidupnya 130 | 131 | ```go 132 | package main 133 | 134 | func main() { 135 | // variabel i akan tetap hidup walaupun looping for sudah selesai 136 | i := 0 137 | for i < 10 { 138 | println(i) 139 | i++ 140 | } 141 | 142 | // variabel i hanya hidup dalam blok for 143 | for i := 0; i < 10; i++ { 144 | println(i) 145 | } 146 | 147 | myMap := map[string]string{"Satu": "Ahad", "Dua": "Senin", "Tiga": "Selasa"} 148 | 149 | // variabel value dan ok tetap hidup walaupun blok if / if else sudah berakhir 150 | value, ok := myMap["Satu"] 151 | if ok { 152 | println(value) 153 | } 154 | 155 | // variabel value dan ok hanya hidup dalam blok if / if else 156 | if value, ok := myMap["Dua"]; ok { 157 | println(value) 158 | } 159 | 160 | myName := string("Jet Lee") 161 | { 162 | name := string("Jacky") 163 | println(name) 164 | } 165 | 166 | println(myName) 167 | } 168 | ``` 169 | 170 | ### Package, Export dan Import 171 | 172 | * Semua type, var, const, func dalam suatu paket yang sama bisa dipanggil di manapun \(meskipun berbeda file\) 173 | * Untuk bisa dipanggil di paket lain, type, var, const, func harus diexport terlebih dahulu 174 | * Cara export dilakukan dengan memberi nama var, type, const, fungsi dll yang diawali dengan huruf besar 175 | * Jika suatu paket ingin menggunakan kode dari paket lainnya, harus mengimport terlebih dahulu 176 | 177 | ```go 178 | // file APP/latihan/satu.go 179 | package latihan 180 | 181 | // MyStr adalah type yang diexport 182 | type MyStr string 183 | 184 | // Salam adalah fungsi diexport 185 | func Salam (m MyStr) { 186 | println(m) 187 | cetaknama() 188 | } 189 | 190 | // cetaknama adalah fungsi privat yang tidak diexport 191 | func cetaknama () { 192 | println(nama) 193 | } 194 | ``` 195 | 196 | ```go 197 | // file APP/latihan/dua.go 198 | package latihan 199 | 200 | // nama var tidak diexport, tapi bisa gunakan di seluruh program dalam paket latihan 201 | var nama string = "Jacky" 202 | 203 | // PrintNama merupakan fungsi yang diexport 204 | func PrintNama() { 205 | println(nama) 206 | } 207 | ``` 208 | 209 | ```go 210 | // file APP/main.go 211 | package main 212 | 213 | import "golang-latihan/latihan" 214 | 215 | func main() { 216 | str := latihan.MyStr("Selamat Pagi") 217 | println(str) 218 | 219 | latihan.Salam(latihan.MyStr("Selamat Sore")) 220 | 221 | //latihan.cetaknama() 222 | latihan.PrintNama() 223 | } 224 | ``` 225 | 226 | ### Type Casting 227 | 228 | * Untuk mengubah suatu tipe menjadi tipe lain, bisa melalui fungsi bawaan golang maupun menggunakan paket strconv. 229 | * Lebih jauh tentang strconv bisa melihat langsung ke paket strconv 230 | 231 | ```go 232 | package main 233 | 234 | func main() { 235 | var myInt int 236 | myInt = 1 237 | 238 | var myUint uint 239 | myUint = uint(myInt) 240 | println(myUint) 241 | 242 | myUint32 := uint32(1) 243 | myUint64 := uint64(myUint32) 244 | println(myUint64) 245 | 246 | str := string("1") 247 | myInt, err := strconv.Atoi(str) 248 | if err != nil { 249 | panic(err) 250 | } 251 | 252 | println(myInt) 253 | 254 | str = strconv.Itoa(myInt) 255 | println(str) 256 | } 257 | ``` 258 | 259 | ### Pointer 260 | 261 | ```go 262 | func main() { 263 | i := 10 264 | p := &i // menunjuk ke i 265 | println(*p) // baca i lewat pointer 266 | *p = 20 // set i lewat pointer 267 | println(i) // lihat nilai terbaru dari i 268 | } 269 | ``` 270 | 271 | ### Function 272 | 273 | * fungsi juga merupakan sebuah tipe 274 | 275 | ```go 276 | // contoh membuat type berupa fungsi 277 | type Handler func(http.ResponseWriter, *http.Request) 278 | type CustomeHandler func(http.ResponseWriter, *http.Request) error 279 | ``` 280 | 281 | * Format fungsi : func NAMA \(argument type\) type\_return 282 | 283 | ```go 284 | // Contoh membuat suatu fungsi 285 | func Jumlah (a int, b int) int { 286 | return a+b 287 | } 288 | ``` 289 | 290 | ```go 291 | type operasi func(a int, b int) int 292 | func main() { 293 | // Lambda 294 | println(func() string { 295 | return "lambda" 296 | }()) 297 | 298 | // Closure 299 | var GetClosure = func() string { 300 | return "closure" 301 | } 302 | 303 | var closure string 304 | closure = GetClosure() 305 | println(closure) 306 | 307 | // Callback dengan lambda :: double square 308 | println(square(func(i int) int { 309 | return i * i 310 | }, 2)) 311 | 312 | // Callback dengan closure, dengan tipe data implisit 313 | var Jumlah = func(a int, b int) int { 314 | return a + b 315 | } 316 | 317 | // Callback dengan closure, dengan tipe data explisit 318 | var Kurang operasi = func(a int, b int) int { 319 | return a - b 320 | } 321 | 322 | println("Operasi Jumlah : ", Hitung(Jumlah, 5, 3)) 323 | println("Operasi Kurang : ", Hitung(Kurang, 5, 3)) 324 | } 325 | 326 | func square(f func(int) int, x int) int { 327 | return f(x * x) 328 | } 329 | 330 | func Hitung(o operasi, x int, y int) int { 331 | return o(x, y) 332 | } 333 | ``` 334 | 335 | ### Variadic Function 336 | 337 | * Merupakan fungsi dengan jumlah argumen yang dinamis. 338 | * Bisa dipanggil dengan cara biasa dengan argumen individual 339 | * Bisa dipanggil secara dinamis dengan melempar argumen slice... 340 | 341 | ```go 342 | func sum(nums ...int) { 343 | total := 0 344 | for _, num := range nums { 345 | total += num 346 | } 347 | println(total) 348 | } 349 | 350 | func main() { 351 | sum(1) 352 | sum(1, 2) 353 | sum(2, 3, 4) 354 | 355 | nums := []int{1, 2, 3, 4} 356 | sum(nums...) 357 | } 358 | ``` 359 | 360 | ## Flow Control 361 | 362 | ### if 363 | 364 | ```go 365 | if err != nil { 366 | return err 367 | } 368 | 369 | if err := run(); err != nil { 370 | return err 371 | } 372 | 373 | if b := 1; b < 10 { 374 | println("blok if", b) 375 | } else { 376 | println("blok else", b) 377 | } 378 | ``` 379 | 380 | ### switch 381 | 382 | ```go 383 | func main() { 384 | fmt.Print("Go berjalan pada ") 385 | switch os := runtime.GOOS; os { 386 | case "darwin": 387 | fmt.Println("OS X.") 388 | case "linux": 389 | fmt.Println("Linux.") 390 | default: 391 | // freebsd, openbsd, 392 | // plan9, windows... 393 | fmt.Printf("%s.\n", os) 394 | } 395 | 396 | t := time.Now() 397 | switch { 398 | case t.Hour() < 12: 399 | fmt.Println("Selamat pagi!") 400 | case t.Hour() < 17: 401 | fmt.Println("Selamat sore.") 402 | default: 403 | fmt.Println("Selamat malam.") 404 | } 405 | } 406 | ``` 407 | 408 | ### for 409 | 410 | ```go 411 | func main() { 412 | // standard for 413 | for i:=0; i<=10; i++ { 414 | println(i) 415 | } 416 | 417 | // while 418 | i := 0 419 | for i<=10 { 420 | println(i) 421 | i++ 422 | } 423 | 424 | // infinite loop 425 | i = 0 426 | for { 427 | println(i) 428 | if i == 10 { 429 | break 430 | } 431 | i++ 432 | } 433 | 434 | // foreach 435 | array := []uint{0,1,2,3,4,5,6,7,8,9,10} 436 | for index, value := range array { 437 | println(index, value) 438 | } 439 | } 440 | ``` 441 | 442 | ### defer 443 | 444 | * Perintah defer menunda eksekusi dari sebuah fungsi sampai fungsi yang melingkupinya selesai. 445 | * Argumen untuk pemanggilan defer dievaluasi langsung, tapi pemanggilan fungsi tidak dieksekusi sampai fungsi yang melingkupinya selesai. 446 | 447 | ```go 448 | func main() { 449 | defer println("datang") 450 | println("selamat") 451 | } 452 | ``` 453 | 454 | * Jika ada tumpukan perintah defer, maka akan dieksekusi secara LIFO \(last In First Out\) 455 | 456 | ```go 457 | func main() { 458 | defer println("pertama") 459 | for i:=0; i<= 10; i++ { 460 | defer println(i) 461 | } 462 | defer println("terakhir") 463 | println("normal") 464 | } 465 | ``` 466 | 467 | ## Array, Slice dan Map 468 | 469 | ### Array 470 | 471 | ```go 472 | var salam [2]string 473 | salam[0] = "selamat" 474 | salam[1] = "pagi" 475 | fmt.Println(salam) 476 | 477 | greeting := []string{"Good", "Morning"} 478 | fmt.Println(greeting) 479 | ``` 480 | 481 | ### Slice 482 | 483 | * Merupakan potongan dari sebuah array 484 | 485 | ```go 486 | var musim [3]string 487 | musim[0] = "panas" 488 | musim[1] = "panas-sekali" 489 | musim[2] = "super-duper-panas" 490 | fmt.Println(musim) 491 | slice := musim[1:2] 492 | fmt.Println(slice) 493 | slice = musim[:2] 494 | fmt.Println(slice) 495 | slice = musim[1:] 496 | fmt.Println(slice) 497 | ``` 498 | 499 | ### Map 500 | 501 | * kalau di PHP ini seperti assosiatif array. 502 | * index otomatis disort secara alpabet 503 | 504 | ```go 505 | hari := map[string]int{"Senin":1, "Selasa":2, "Rabu":3} 506 | fmt.Println(hari) 507 | ``` 508 | 509 | ### Common Operation 510 | 511 | * mengunakan potongan array \(slice\) sehingga tidak dideklarasikan kapasitasnya 512 | * untuk menambahkan anggota dengan menggunakan fungsi append 513 | 514 | ```go 515 | var salam []string 516 | salam = append(salam, "selamat") 517 | salam = append(salam, "pagi") 518 | fmt.Println(salam) 519 | ``` 520 | 521 | ```go 522 | func main() { 523 | buah := []string{"rambutan", "durian", "salak"} 524 | exist, index := InArray("duku", buah) 525 | println(exist, index) 526 | 527 | buah = Remove("durian", buah) 528 | fmt.Println(buah) 529 | buah = append(buah, "mangga") 530 | fmt.Println(buah) 531 | exist, index = InArray("salak", buah) 532 | if exist { 533 | buah = RemoveByIndex(index, buah) 534 | } 535 | fmt.Println(buah) 536 | } 537 | 538 | func InArray(val string, array []string) (bool, int) { 539 | for i, s := range array { 540 | if s == val { 541 | return true, i 542 | } 543 | } 544 | 545 | return false, -1 546 | } 547 | 548 | func Remove(val string, array []string) []string { 549 | isExist, index := InArray(val, array) 550 | if isExist { 551 | if index == 0 { 552 | array = array[1:] 553 | } else { 554 | array = append(array[:index], array[(index+1):]...) 555 | } 556 | } 557 | 558 | return array 559 | } 560 | 561 | func RemoveByIndex(index int, array []string) []string { 562 | if index == 0 { 563 | return array[1:] 564 | } else { 565 | return append(array[:index], array[(index+1):]...) 566 | } 567 | } 568 | ``` 569 | 570 | -------------------------------------------------------------------------------- /golang-fundamental/konkurensi.md: -------------------------------------------------------------------------------- 1 | # Konkurensi 2 | 3 | Konkurensi adalah komposisi / struktur dari berbagai proses yang berjalan secara bersamaan. Fitur untuk melakukan konkurensi dalam golang adalah Go Routine. 4 | 5 | ## Go Routine 6 | 7 | * Sebuah thread yang ringan, hanya dibutuhkan 2kB memori untuk menjalankan sebuah go routine 8 | * Aksi go routine bersifat asynchronous, jadi tidak saling menunggu dengan go routine yang lain. 9 | * Proses yang hendak dieksekusi sebagai go routine harus berupa fungsi tanpa return yang dipanggil dengan kata kunci go 10 | 11 | ```go 12 | package main 13 | 14 | import "time" 15 | 16 | func Salam(s string) { 17 | for i := 0; i <= 10; i++ { 18 | println(s) 19 | time.Sleep(1000 * time.Millisecond) 20 | } 21 | } 22 | 23 | func main() { 24 | // salam tidak pernah tercetak karena dijalankan secara konkuren. 25 | // Sehingga tidak akan ditunggu oleh func main, dan langsung exit. 26 | go Salam("Selamat Pagi") 27 | } 28 | ``` 29 | 30 | ```go 31 | package main 32 | 33 | import "time" 34 | 35 | func Salam(s string) { 36 | for i := 0; i <= 10; i++ { 37 | println(s) 38 | time.Sleep(1000 * time.Millisecond) 39 | } 40 | } 41 | 42 | func main() { 43 | // salam tidak pernah tercetak karena dijalankan secara konkuren. 44 | // Sehingga tidak akan ditunggu oleh func main, dan langsung exit. 45 | go Salam("Selamat Pagi") 46 | println("Halo") 47 | } 48 | ``` 49 | 50 | ```go 51 | package main 52 | 53 | import "time" 54 | 55 | func Salam(s string) { 56 | for i := 0; i <= 10; i++ { 57 | println(s) 58 | time.Sleep(1000 * time.Millisecond) 59 | } 60 | } 61 | 62 | func main() { 63 | go Salam("Selamat Pagi") 64 | Salam("Selamat Malam") 65 | } 66 | ``` 67 | 68 | * Go routine jalan di multi core processor, dan bisa diset mau jalan di berapa core. 69 | 70 | ```go 71 | package main 72 | 73 | import "time" 74 | 75 | func Salam(s string) { 76 | for i := 0; i <= 10; i++ { 77 | println(s) 78 | time.Sleep(1000 * time.Millisecond) 79 | } 80 | } 81 | 82 | func main() { 83 | runtime.GOMAXPROCS(1) 84 | 85 | go Salam("Selamat Pagi") 86 | Salam("Selamat Malam") 87 | } 88 | ``` 89 | 90 | ## Channel 91 | 92 | * Untuk mengsinkronkan satu go routine dengan go routine lainnya, diperlukan channel 93 | * Channel digunakan untuk menerima dan mengirim data antar go routine. 94 | * Channel bersifat blocking / synchronous. Pengiriman dan penerimaan ditahan sampai sisi yang lain siap. 95 | * Channel harus dibuat sebelum digunakan, dengan kombinasi kata kunci make dan chan 96 | * Aliran untuk menerima / mengirim data ditunjukkan dengan arah panah 97 | 98 | ```go 99 | package main 100 | 101 | import "runtime" 102 | 103 | func main() { 104 | var pesan = make(chan string) 105 | println("kirim data", "Jacky") 106 | pesan <- "Jacky" 107 | 108 | // akan error karena tidak ada go routine lain yang menangkap channel 109 | println("terima data", <-pesan) 110 | } 111 | ``` 112 | 113 | ```go 114 | package main 115 | 116 | func main() { 117 | var pesan = make(chan string) 118 | 119 | println("kirim data", "Jacky") 120 | pesan <- "Jacky" 121 | 122 | // error karena go routine yang menangkap channel belum dieksekusi ketika exit program 123 | go func() { 124 | println("terima data", <-pesan) 125 | }() 126 | 127 | } 128 | ``` 129 | 130 | ```go 131 | package main 132 | 133 | func main() { 134 | var pesan = make(chan string) 135 | 136 | go func() { 137 | println("terima data", <-pesan) 138 | }() 139 | 140 | println("kirim data", "Jacky") 141 | pesan <- "Jacky" 142 | 143 | } 144 | ``` 145 | 146 | ```go 147 | package main 148 | 149 | func main() { 150 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 151 | 152 | var pesan = make(chan string) 153 | 154 | // error karena go routine yang melakukan penerimaan data hanya sekali, sementara pengiriman dilakukan 4 kali 155 | go func() { 156 | println("terima data", <-pesan) 157 | }() 158 | 159 | for _, s := range a { 160 | println("kirim data", s) 161 | pesan <- s 162 | } 163 | } 164 | ``` 165 | 166 | ```go 167 | package main 168 | 169 | func main() { 170 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 171 | 172 | var pesan = make(chan string) 173 | 174 | go func() { 175 | // catatan: looping tanpa henti termasuk boros cpu, 176 | // di materi selanjutnya ada cara tanpa menggunakan for{} 177 | // baik melalui for range maupun for break 178 | for { 179 | println("terima data", <-pesan) 180 | } 181 | }() 182 | 183 | for _, s := range a { 184 | println("kirim data", s) 185 | pesan <- s 186 | } 187 | } 188 | ``` 189 | 190 | ## Channel dengan buffer 191 | 192 | * Panjang buffer ditambahkan pada fungsi make sebagai argumen kedua 193 | * Buffering menyebabkan pengiriman dan penerimaan data berlangsung secara asynchronous 194 | * Pengiriman ke kanal buffer akan ditahan bila buffer telah penuh. Penerimaan akan ditahan saat buffer kosong. 195 | * Jika pengiriman data melebihi panjang buffer, maka akan diperlakukan secara synchronous. 196 | 197 | ```go 198 | package main 199 | 200 | func main() { 201 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 202 | 203 | var pesan = make(chan string, 3) 204 | 205 | go func() { 206 | for { 207 | println("terima data", <-pesan) 208 | } 209 | }() 210 | 211 | for _, s := range a { 212 | println("kirim data", s) 213 | pesan <- s 214 | } 215 | } 216 | ``` 217 | 218 | ```go 219 | package main 220 | 221 | func main() { 222 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 223 | 224 | var pesan = make(chan string, len(a)) 225 | 226 | go func() { 227 | println("terima data", <-pesan) 228 | }() 229 | 230 | for _, s := range a { 231 | println("kirim data", s) 232 | pesan <- s 233 | } 234 | } 235 | ``` 236 | 237 | ```go 238 | package main 239 | 240 | func main() { 241 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 242 | var pesan = make(chan string, len(a)) 243 | for _, s := range a { 244 | println("kirim data", s) 245 | pesan <- s 246 | } 247 | } 248 | ``` 249 | 250 | ## Range dan Close 251 | 252 | * Range merupakan perulangan dari sebuah channel 253 | 254 | ```go 255 | package main 256 | 257 | func main() { 258 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 259 | 260 | var pesan = make(chan string, len(a)-1) 261 | 262 | go func() { 263 | for i := range pesan { 264 | println("terima data", i) 265 | } 266 | }() 267 | 268 | for _, s := range a { 269 | println("kirim data", s) 270 | pesan <- s 271 | } 272 | 273 | } 274 | ``` 275 | 276 | * Pengirim bisa menutup sebuah channel untuk menandai sudah tidak ada data yang dikirim lagi. 277 | * Penutupan ini hanya optional. Artinya pengirim boleh melakukan close maupun tidak. 278 | * Yang melakukan close hanya pengirim. Karena jika yang melakukan close adalah penerima, dan ada routine yang melakukan pengrimana akan menyebabkan panic. 279 | * Penerima bisa menambahkan pengecekan, jika masih ada data yang dikirim maka akan diterima. 280 | 281 | ```go 282 | package main 283 | 284 | func main() { 285 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 286 | 287 | var pesan = make(chan string, len(a)-1) 288 | 289 | go func() { 290 | for { 291 | println("terima data", <-pesan) 292 | close(pesan) 293 | } 294 | }() 295 | 296 | for _, s := range a { 297 | println("kirim data", s) 298 | pesan <- s 299 | } 300 | } 301 | ``` 302 | 303 | ```go 304 | package main 305 | 306 | func main() { 307 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 308 | 309 | var pesan = make(chan string, len(a)-1) 310 | 311 | go func() { 312 | for { 313 | println("terima data", <-pesan) 314 | } 315 | }() 316 | 317 | for _, s := range a { 318 | println("kirim data", s) 319 | pesan <- s 320 | } 321 | close(pesan) 322 | } 323 | ``` 324 | 325 | ```go 326 | package main 327 | 328 | func main() { 329 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 330 | 331 | var pesan = make(chan string, len(a)) 332 | 333 | for _, s := range a { 334 | println("kirim data", s) 335 | pesan <- s 336 | } 337 | close(pesan) 338 | } 339 | ``` 340 | 341 | ```go 342 | package main 343 | 344 | func main() { 345 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 346 | 347 | var pesan = make(chan string, len(a)) 348 | 349 | for _, s := range a { 350 | println("kirim data", s) 351 | pesan <- s 352 | } 353 | close(pesan) 354 | 355 | for { 356 | println("terima data", <-pesan) 357 | } 358 | } 359 | ``` 360 | 361 | ```go 362 | package main 363 | 364 | func main() { 365 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 366 | 367 | var pesan = make(chan string, len(a)) 368 | 369 | for _, s := range a { 370 | println("kirim data", s) 371 | pesan <- s 372 | } 373 | close(pesan) 374 | 375 | for { 376 | // dilakukan pengecekan agar tidak looping forefer 377 | if v, ok := <-pesan; ok { 378 | println("terima data", v) 379 | } else { 380 | break 381 | } 382 | } 383 | } 384 | ``` 385 | 386 | ```go 387 | package main 388 | 389 | func main() { 390 | a := []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} 391 | 392 | var pesan = make(chan string, len(a)) 393 | 394 | // menggunakan range jauh lebih simple 395 | for _, s := range a { 396 | println("kirim data", s) 397 | pesan <- s 398 | } 399 | close(pesan) 400 | 401 | for i := range pesan { 402 | println("terima data", i) 403 | } 404 | } 405 | ``` 406 | 407 | ## Select 408 | 409 | * Channel diperlukan untuk pertukaran data antar go routine 410 | * Jika melibatkan lebih dari satu go routine, diperlukan fungsi kontrol melalui select 411 | * Select akan menerima secara acak mana data yang terlebih dahulu tersedia 412 | 413 | ```go 414 | package main 415 | 416 | func main() { 417 | var pesan = make(chan string) 418 | var c = make(chan int) 419 | 420 | go func() { 421 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 422 | pesan <- s 423 | } 424 | }() 425 | 426 | go func() { 427 | c <- 5 428 | }() 429 | 430 | select { 431 | case i := <-c: 432 | println("terima data", i) 433 | case s := <-pesan: 434 | println("terima data", s) 435 | } 436 | } 437 | ``` 438 | 439 | ```go 440 | package main 441 | 442 | func main() { 443 | var pesan = make(chan string) 444 | var c = make(chan int) 445 | 446 | go func() { 447 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 448 | pesan <- s 449 | } 450 | }() 451 | 452 | go func() { 453 | c <- 5 454 | }() 455 | 456 | for a := 0; a <= 4; a++ { 457 | select { 458 | case i := <-c: 459 | println("terima data", i) 460 | case s := <-pesan: 461 | println("terima data", s) 462 | } 463 | } 464 | } 465 | ``` 466 | 467 | ```go 468 | package main 469 | 470 | func main() { 471 | var pesan = make(chan string) 472 | var c = make(chan int) 473 | 474 | go func() { 475 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 476 | pesan <- s 477 | } 478 | }() 479 | 480 | go func() { 481 | c <- 5 482 | }() 483 | 484 | for a := 0; a <= 5; a++ { 485 | select { 486 | case i := <-c: 487 | println("terima data", i) 488 | case s := <-pesan: 489 | println("terima data", s) 490 | } 491 | } 492 | } 493 | ``` 494 | 495 | ## Select Default 496 | 497 | * Jika saat select tidak ada channel yang siap diterima maka akan dijalankan baris kode default 498 | 499 | ```go 500 | package main 501 | 502 | func main() { 503 | var pesan = make(chan string) 504 | var c = make(chan int) 505 | 506 | go func() { 507 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 508 | pesan <- s 509 | } 510 | }() 511 | 512 | go func() { 513 | c <- 5 514 | }() 515 | 516 | for a := 0; a <= 5; a++ { 517 | select { 518 | case i := <-c: 519 | println("terima data", i) 520 | case s := <-pesan: 521 | println("terima data", s) 522 | default : 523 | println("tidak ada penerimaan data") 524 | } 525 | } 526 | } 527 | ``` 528 | 529 | ## Select Timeout 530 | 531 | * Teknik tambahan untuk mengakhiri select jika tidak ada penerimaan data 532 | 533 | ```go 534 | package main 535 | 536 | import ( 537 | "fmt" 538 | "time" 539 | ) 540 | 541 | func main() { 542 | var pesan = make(chan string) 543 | var c = make(chan int) 544 | 545 | go func() { 546 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 547 | pesan <- s 548 | } 549 | }() 550 | 551 | go func() { 552 | c <- 5 553 | }() 554 | 555 | loop: 556 | for { 557 | select { 558 | case i := <-c: 559 | println("terima data", i) 560 | case s := <-pesan: 561 | println("terima data", s) 562 | case <-time.After(time.Second * 5): 563 | fmt.Println("timeout. tidak ada aktivitas selama 5 detik") 564 | break loop 565 | } 566 | } 567 | } 568 | ``` 569 | 570 | Hati-hati jika ingin menggabungkan antara select timeout dengan default, karena bisa terjadi looping forever. 571 | 572 | ```go 573 | package main 574 | 575 | import ( 576 | "fmt" 577 | "time" 578 | ) 579 | 580 | func main() { 581 | var pesan = make(chan string) 582 | var c = make(chan int) 583 | 584 | go func() { 585 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 586 | pesan <- s 587 | } 588 | }() 589 | 590 | go func() { 591 | c <- 5 592 | }() 593 | 594 | loop: 595 | for { 596 | select { 597 | case i := <-c: 598 | println("terima data", i) 599 | case s := <-pesan: 600 | println("terima data", s) 601 | case <-time.After(time.Second * 5): 602 | fmt.Println("timeout. tidak ada aktivitas selama 5 detik") 603 | break loop 604 | default: 605 | println("tidak ada data diterima") 606 | } 607 | } 608 | } 609 | ``` 610 | 611 | Ini disebabkan time.After\(time.Second \* 5\) selalu dibuat setiap looping seleksi, untuk mengatasinya, buat variable untuk menampung timeout agar timeout dikenali disetiap looping. 612 | 613 | ```go 614 | package main 615 | 616 | import ( 617 | "fmt" 618 | "time" 619 | ) 620 | 621 | func main() { 622 | var pesan = make(chan string) 623 | var c = make(chan int) 624 | 625 | go func() { 626 | for _, s := range []string{"Jacky", "Jet Lee", "Bruce Lee", "Samo Hung"} { 627 | pesan <- s 628 | } 629 | }() 630 | 631 | go func() { 632 | c <- 5 633 | }() 634 | 635 | timeout := time.After(time.Second * 5) 636 | 637 | loop: 638 | for { 639 | select { 640 | case i := <-c: 641 | println("terima data", i) 642 | case s := <-pesan: 643 | println("terima data", s) 644 | case <-timeout: 645 | fmt.Println("timeout. tidak ada aktivitas selama 5 detik") 646 | break loop 647 | default: 648 | println("tidak ada data diterima") 649 | } 650 | } 651 | } 652 | ``` 653 | 654 | ## Sync Mutex 655 | 656 | * Channel dipakai untuk komunikasi antar go routine 657 | * Jika tidak ingin berkomunikasi karena ngin memastikan hanya satu goroutine yang dapat mengakses suatu variabel pada satu waktu untuk menghindari konflik, digunakan sync mutex 658 | * mutex adalah mutual exclusion dengan fungsi `Lock` dan `Unlock` 659 | 660 | ```go 661 | package main 662 | 663 | import ( 664 | "fmt" 665 | "sync" 666 | "time" 667 | ) 668 | 669 | // SafeCounter aman digunakan secara konkuren. 670 | type SafeCounter struct { 671 | v map[string]int 672 | mux sync.Mutex 673 | } 674 | 675 | // Inc meningkatkan nilai dari key. 676 | func (c *SafeCounter) Inc(key string) { 677 | c.mux.Lock() 678 | // Lock sehingga hanya satu goroutine pada satu waktu yang dapat 679 | // mengakses map c.v. 680 | c.v[key]++ 681 | c.mux.Unlock() 682 | } 683 | 684 | // Value mengembalikan nilai dari key. 685 | func (c *SafeCounter) Value(key string) int { 686 | c.mux.Lock() 687 | // Lock sehingga hanya satu gorouting pada satu waktu yang dapat 688 | // mengakses map c.v. 689 | defer c.mux.Unlock() 690 | return c.v[key] 691 | } 692 | 693 | func main() { 694 | c := SafeCounter{v: make(map[string]int)} 695 | for i := 0; i < 1000; i++ { 696 | go c.Inc("key") 697 | } 698 | 699 | time.Sleep(time.Second) 700 | fmt.Println(c.Value("key")) 701 | } 702 | ``` 703 | 704 | ## Sync.WaitGroup 705 | 706 | * Kadang kita perlu menjalankan satu group routine yang terdiri dari beberapa go routine. 707 | * Kita ingin mengontrol group rutin tersebut dengan melakukan sinkronisasi \(synchronous\). 708 | * Fitur sync.WaitGroup memungkinkan kita untuk menunggu semua group routine selesai. 709 | * Untuk mencegah suatu routine berlangsung lama dibanding routine lainnya, dipasang context dengan deadline. 710 | * Jika ada satu error di salah satu routine, maka seluruh routine yang sedang jalan akan dicancel. 711 | 712 | ### Group Routine 713 | 714 | ```go 715 | package main 716 | 717 | import "fmt" 718 | 719 | func main() { 720 | for i := 0; i < 10; i++ { 721 | go fmt.Printf("Routine ke: %d\n", i) 722 | } 723 | } 724 | ``` 725 | 726 | * Jika dijalankan kemungkinan tidak ada hasil yang diprint, atau mungkin cuma ada 1x print. 727 | * Tidak ada garansi apakah suatu routine bisa selesai dieksekusi. 728 | * Go menjalankan fungsi main, dan ketika fungsi main berakhir, maka berakhir juga seluruh program. 729 | * Kode di atas menjalankan sekelompok goroutine dan kemudian keluar sebelum mereka punya waktu untuk eksekusi. 730 | 731 | ### Wait Group 732 | 733 | * Solusi untuk kasus di atas adalah dengan menggunakan standar library sync.WaitGroup 734 | 735 | ```go 736 | package main 737 | 738 | import ( 739 | "fmt" 740 | "sync" 741 | ) 742 | 743 | func main() { 744 | var wg sync.WaitGroup 745 | for i := 0; i < 10; i++ { 746 | wg.Add(1) 747 | go func (id int) { 748 | defer wg.Done() 749 | fmt.Printf("Routine dengan id: %d\n", id) 750 | }(i) 751 | } 752 | wg.Wait() 753 | } 754 | ``` 755 | 756 | * wg.Add\(\) untuk counter berapa goroutine yang sudah ditambahkan. Setiap kali hendak menjalankan gouroutine, tambahkan counter dengan perintah wg,Add\(1\). 757 | * wg.Done\(\) untuk menandai suatu routine sudah selesai 758 | * wg.Wait\(\) untuk menunggu seluruh counter routine sudah nol \(semua goroutine telah selesai\). 759 | * Perhatikan saya mengenalkan variabel local id sebagai id sebuah goroutine. Ini adalah mekanisme aman menggunakan variabel local. Karena jika menggunakan varibel luar i, akan terjadi konflik karena menjalankan potensi race condition. 760 | * Di bawah ini adalah contoh kode yang salah karena tidak menggunakan variabel local. 761 | 762 | ```go 763 | package main 764 | 765 | import ( 766 | "fmt" 767 | "sync" 768 | ) 769 | 770 | func main() { 771 | var wg sync.WaitGroup 772 | for i := 0; i < 10; i++ { 773 | wg.Add(1) 774 | go func() { 775 | defer wg.Done() 776 | fmt.Printf("Routine dengan id: %d\n", i) 777 | }() 778 | } 779 | wg.Wait() 780 | } 781 | ``` 782 | 783 | ### Handling Error 784 | 785 | * Kode di atas sederhana dan optimis tidak terjadi error, padahal aplikasi riil pasti ada penanganan error. 786 | * Library golang.org/x/sync/errgroup digunakan untuk handling error. 787 | * Ingat untuk menggunakan variabel local sebagai id 788 | * Error yang ditangkap adalah error pertama yang dihasilkan oleh routine. 789 | 790 | ```go 791 | package main 792 | 793 | import ( 794 | "fmt" 795 | 796 | "golang.org/x/sync/errgroup" 797 | ) 798 | 799 | func main() { 800 | var eg errgroup.Group 801 | for i := 0; i < 10; i++ { 802 | id := i 803 | eg.Go(func() error { 804 | return routine(id) 805 | }) 806 | } 807 | 808 | if err := eg.Wait(); err != nil { 809 | fmt.Println("terjadi error: ", err) 810 | return 811 | } 812 | 813 | fmt.Println("sukses") 814 | } 815 | 816 | func routine(id int) error { 817 | fmt.Printf("Routine dengan id: %d\n", id) 818 | 819 | if id == 9 || id == 6 { 820 | return fmt.Errorf("simulasi error %d", id) 821 | } 822 | 823 | return nil 824 | } 825 | ``` 826 | 827 | ### Context 828 | 829 | * context berguna untuk menjaga agar context dari client bisa diiikuti. 830 | * context bisa digunakan untuk menyimpan variabel yang siklus hiduonya sesuai context. 831 | * context bisa digunakan untuk melakukan deadline maupun cancell ation suatu fungsi. 832 | * context bisa digunakan untuk melakukan cancellation kode saat context sdh berakhir. 833 | 834 | ```go 835 | package main 836 | 837 | import ( 838 | "context" 839 | "fmt" 840 | 841 | "golang.org/x/sync/errgroup" 842 | ) 843 | 844 | func main() { 845 | eg, ctx := errgroup.WithContext(context.Background()) 846 | for i := 0; i < 10; i++ { 847 | id := i 848 | eg.Go(func() error { 849 | return routineContext(ctx, id) 850 | }) 851 | } 852 | 853 | if err := eg.Wait(); err != nil { 854 | fmt.Println("terjadi error: ", err) 855 | return 856 | } 857 | 858 | fmt.Println("sukses") 859 | } 860 | 861 | func routineContext(ctx context.Context, id int) error { 862 | select { 863 | case <-ctx.Done(): 864 | fmt.Printf("context cancelled job %v terminting\n", id) 865 | return ctx.Err() 866 | default: 867 | } 868 | 869 | fmt.Printf("Routine dengan id: %d\n", id) 870 | 871 | if id == 9 || id == 6 { 872 | return fmt.Errorf("simulasi error %d", id) 873 | } 874 | 875 | return nil 876 | } 877 | ``` 878 | 879 | Kita bisa menambahkan deadline suatu context 880 | 881 | ```go 882 | package main 883 | 884 | import ( 885 | "context" 886 | "fmt" 887 | "time" 888 | 889 | "golang.org/x/sync/errgroup" 890 | ) 891 | 892 | func main() { 893 | ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond) 894 | defer cancel() 895 | 896 | eg, ctx := errgroup.WithContext(ctx) 897 | for i := 0; i < 10; i++ { 898 | id := i 899 | eg.Go(func() error { 900 | return routineContext(ctx, id) 901 | }) 902 | } 903 | 904 | if err := eg.Wait(); err != nil { 905 | fmt.Println("terjadi error: ", err) 906 | return 907 | } 908 | 909 | fmt.Println("sukses") 910 | } 911 | 912 | func routineContext(ctx context.Context, id int) error { 913 | select { 914 | case <-ctx.Done(): 915 | fmt.Printf("context cancelled job %v terminting\n", id) 916 | return ctx.Err() 917 | default: 918 | } 919 | 920 | fmt.Printf("Routine dengan id: %d\n", id) 921 | 922 | if id == 9 || id == 6 { 923 | return fmt.Errorf("simulasi error %d", id) 924 | } 925 | 926 | return nil 927 | } 928 | ``` 929 | 930 | Bandingkan jika kita tidak handle context, maka cancellation jadi tidak berfungsi. 931 | 932 | ```go 933 | package main 934 | 935 | import ( 936 | "context" 937 | "fmt" 938 | "time" 939 | 940 | "golang.org/x/sync/errgroup" 941 | ) 942 | 943 | func main() { 944 | ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond) 945 | defer cancel() 946 | 947 | eg, ctx := errgroup.WithContext(ctx) 948 | for i := 0; i < 10; i++ { 949 | id := i 950 | eg.Go(func() error { 951 | return routineContext(ctx, id) 952 | }) 953 | } 954 | 955 | if err := eg.Wait(); err != nil { 956 | fmt.Println("terjadi error: ", err) 957 | return 958 | } 959 | 960 | fmt.Println("sukses") 961 | } 962 | 963 | func routineContext(ctx context.Context, id int) error { 964 | /*select { 965 | case <-ctx.Done(): 966 | fmt.Printf("context cancelled job %v terminting\n", id) 967 | return ctx.Err() 968 | default: 969 | } */ 970 | 971 | fmt.Printf("Routine dengan id: %d\n", id) 972 | 973 | if id == 9 || id == 6 { 974 | return fmt.Errorf("simulasi error %d", id) 975 | } 976 | 977 | return nil 978 | } 979 | ``` 980 | 981 | -------------------------------------------------------------------------------- /grpc-framework/grpc-clean-architecture.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture 2 | 3 | * kita sudah pernah mempelajari [clean arcitecture](../build-rest-api-framework/clean-architecture.md) 4 | * kali ini kita akan menggunakan konsep domain 5 | * structure folder sebagai berikut 6 | 7 | ```text 8 | ---- [domain] 9 | ---- [ddrivers] 10 | ---- [handler] 11 | grpc.go 12 | ---- [repositories] 13 | repo.go 14 | ---- [usecase] 15 | usecase.go 16 | otherfile.go 17 | ---- [validation] 18 | validationfile.go 19 | repository_interface.go 20 | usecase_interface.go 21 | validation_interface.go 22 | ``` 23 | 24 | * Kita akan memecah file domain/ddrivers/handler.go menjadi mengikuti structur folder di atas. 25 | 26 | ## Helper 27 | 28 | * untuk fungsi-fungsi pendukung akan dikelompokkan ke dalam folder lib/helper 29 | * Buat file lib/helper/error\_ctx.go 30 | 31 | ```go 32 | package helper 33 | 34 | import ( 35 | "context" 36 | 37 | "google.golang.org/grpc/codes" 38 | "google.golang.org/grpc/status" 39 | ) 40 | 41 | func ContextError(ctx context.Context) error { 42 | switch ctx.Err() { 43 | case context.Canceled: 44 | return status.Error(codes.Canceled, context.Canceled.Error()) 45 | case context.DeadlineExceeded: 46 | return status.Error(codes.DeadlineExceeded, context.DeadlineExceeded.Error()) 47 | default: 48 | return nil 49 | } 50 | } 51 | ``` 52 | 53 | ## Repository 54 | 55 | * repository adalah kode-kode yang mengakses transaksi database 56 | * Buat file domain/ddrivers/repository\_interface.go 57 | 58 | ```go 59 | package ddrivers 60 | 61 | import ( 62 | "context" 63 | "skeleton/pb/drivers" 64 | "skeleton/pb/generic" 65 | ) 66 | 67 | type DriverRepoInterface interface { 68 | Find(ctx context.Context, id string) error 69 | FindAll(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) 70 | Create(ctx context.Context) error 71 | Update(ctx context.Context) error 72 | Delete(ctx context.Context, in *generic.Id) error 73 | GetPb() *drivers.Driver 74 | SetPb(*drivers.Driver) 75 | } 76 | ``` 77 | 78 | * Buat folder domain/ddrivers/repositories. Semua file yang mengimplementasikan repository interface akan dibuat dalam filder ini. 79 | * Buat file domain/ddrivers/repositories/repo.go 80 | 81 | ```go 82 | package repositories 83 | 84 | import ( 85 | "database/sql" 86 | "log" 87 | "skeleton/domain/ddrivers" 88 | "skeleton/pb/drivers" 89 | ) 90 | 91 | type repo struct { 92 | db *sql.DB 93 | log *log.Logger 94 | pb drivers.Driver 95 | } 96 | 97 | func NewDriverRepo(db *sql.DB, log *log.Logger) ddrivers.DriverRepoInterface { 98 | return &repo{ 99 | db: db, 100 | log: log, 101 | } 102 | } 103 | 104 | func (u *repo) GetPb() *drivers.Driver { 105 | return &u.pb 106 | } 107 | 108 | func (u *repo) SetPb(in *drivers.Driver) { 109 | if len(in.Id) > 0 { 110 | u.pb.Id = in.Id 111 | } 112 | if len(in.Name) > 0 { 113 | u.pb.Name = in.Name 114 | } 115 | if len(in.Phone) > 0 { 116 | u.pb.Phone = in.Phone 117 | } 118 | if len(in.LicenceNumber) > 0 { 119 | u.pb.LicenceNumber = in.LicenceNumber 120 | } 121 | if len(in.CompanyId) > 0 { 122 | u.pb.CompanyId = in.CompanyId 123 | } 124 | if len(in.CompanyName) > 0 { 125 | u.pb.CompanyName = in.CompanyName 126 | } 127 | u.pb.IsDelete = in.IsDelete 128 | if len(in.Created) > 0 { 129 | u.pb.Created = in.Created 130 | } 131 | if len(in.CreatedBy) > 0 { 132 | u.pb.CreatedBy = in.CreatedBy 133 | } 134 | if len(in.Updated) > 0 { 135 | u.pb.Updated = in.Updated 136 | } 137 | if len(in.UpdatedBy) > 0 { 138 | u.pb.UpdatedBy = in.UpdatedBy 139 | } 140 | } 141 | ``` 142 | 143 | * Buat file domain/ddrivers/repositories/find\_all.go 144 | 145 | ```go 146 | package repositories 147 | 148 | import ( 149 | "context" 150 | "fmt" 151 | "skeleton/lib/helper" 152 | "skeleton/pb/drivers" 153 | "skeleton/pb/generic" 154 | "strconv" 155 | "strings" 156 | 157 | "google.golang.org/grpc/codes" 158 | "google.golang.org/grpc/status" 159 | ) 160 | 161 | func (u *repo) FindAll(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 162 | select { 163 | case <-ctx.Done(): 164 | return nil, helper.ContextError(ctx) 165 | default: 166 | } 167 | 168 | out := &drivers.Drivers{} 169 | query := `SELECT id, name, phone, licence_number, company_id, company_name FROM drivers` 170 | where := []string{"is_deleted = false"} 171 | paramQueries := []interface{}{} 172 | 173 | if len(in.Ids) > 0 { 174 | orWhere := []string{} 175 | for _, id := range in.Ids { 176 | paramQueries = append(paramQueries, id) 177 | orWhere = append(orWhere, fmt.Sprintf("id = %d", len(paramQueries))) 178 | } 179 | if len(orWhere) > 0 { 180 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 181 | } 182 | } 183 | 184 | if len(in.CompanyIds) > 0 { 185 | orWhere := []string{} 186 | for _, id := range in.CompanyIds { 187 | paramQueries = append(paramQueries, id) 188 | orWhere = append(orWhere, fmt.Sprintf("company_id = %d", len(paramQueries))) 189 | } 190 | if len(orWhere) > 0 { 191 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 192 | } 193 | } 194 | 195 | if len(in.LicenceNumbers) > 0 { 196 | orWhere := []string{} 197 | for _, licenceNumber := range in.LicenceNumbers { 198 | paramQueries = append(paramQueries, licenceNumber) 199 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 200 | } 201 | if len(orWhere) > 0 { 202 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 203 | } 204 | } 205 | 206 | if len(in.Names) > 0 { 207 | orWhere := []string{} 208 | for _, name := range in.Names { 209 | paramQueries = append(paramQueries, name) 210 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 211 | } 212 | if len(orWhere) > 0 { 213 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 214 | } 215 | } 216 | 217 | if len(in.Phones) > 0 { 218 | orWhere := []string{} 219 | for _, phone := range in.Phones { 220 | paramQueries = append(paramQueries, phone) 221 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 222 | } 223 | if len(orWhere) > 0 { 224 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 225 | } 226 | } 227 | 228 | if in.Pagination == nil { 229 | in.Pagination = &generic.Pagination{} 230 | } 231 | 232 | if len(in.Pagination.Keyword) > 0 { 233 | orWhere := []string{} 234 | 235 | paramQueries = append(paramQueries, in.Pagination.Keyword) 236 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 237 | 238 | paramQueries = append(paramQueries, in.Pagination.Keyword) 239 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 240 | 241 | paramQueries = append(paramQueries, in.Pagination.Keyword) 242 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 243 | 244 | paramQueries = append(paramQueries, in.Pagination.Keyword) 245 | orWhere = append(orWhere, fmt.Sprintf("company_name = %d", len(paramQueries))) 246 | 247 | if len(orWhere) > 0 { 248 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 249 | } 250 | } 251 | 252 | if len(in.Pagination.Sort) > 0 { 253 | in.Pagination.Sort = strings.ToLower(in.Pagination.Sort) 254 | if in.Pagination.Sort != "asc" { 255 | in.Pagination.Sort = "desc" 256 | } 257 | } else { 258 | in.Pagination.Sort = "desc" 259 | } 260 | 261 | if len(in.Pagination.Order) > 0 { 262 | in.Pagination.Order = strings.ToLower(in.Pagination.Order) 263 | if !(in.Pagination.Order == "id" || 264 | in.Pagination.Order == "name" || 265 | in.Pagination.Order == "phone" || 266 | in.Pagination.Order == "licence_number" || 267 | in.Pagination.Order == "company_id" || 268 | in.Pagination.Order == "company_name") { 269 | in.Pagination.Order = "id" 270 | } 271 | } else { 272 | in.Pagination.Order = "id" 273 | } 274 | 275 | if in.Pagination.Limit <= 0 { 276 | in.Pagination.Limit = 10 277 | } 278 | 279 | if in.Pagination.Offset <= 0 { 280 | in.Pagination.Offset = 0 281 | } 282 | 283 | if len(where) > 0 { 284 | query += " WHERE " + strings.Join(where, " AND ") 285 | } 286 | 287 | query += " ORDER BY " + in.Pagination.Order + " " + in.Pagination.Sort 288 | query += " LIMIT " + strconv.Itoa(int(in.Pagination.Limit)) 289 | query += " OFFSET " + strconv.Itoa(int(in.Pagination.Offset)) 290 | 291 | rows, err := u.db.QueryContext(ctx, query, paramQueries...) 292 | if err != nil { 293 | u.log.Println(err.Error()) 294 | return nil, status.Error(codes.Internal, err.Error()) 295 | } 296 | defer rows.Close() 297 | 298 | for rows.Next() { 299 | var obj drivers.Driver 300 | err = rows.Scan(&obj.Id, &obj.Name, &obj.Phone, &obj.LicenceNumber, &obj.CompanyId, &obj.CompanyName) 301 | if err != nil { 302 | u.log.Println(err.Error()) 303 | return nil, status.Error(codes.Internal, err.Error()) 304 | } 305 | 306 | out.Driver = append(out.Driver, &obj) 307 | } 308 | 309 | if rows.Err() != nil { 310 | u.log.Println(rows.Err().Error()) 311 | return nil, status.Error(codes.Internal, rows.Err().Error()) 312 | } 313 | 314 | return out, nil 315 | } 316 | ``` 317 | 318 | * Buat file domain/ddrivers/repositories/find\_driver\_by\_id.go 319 | 320 | ```go 321 | package repositories 322 | 323 | import ( 324 | "context" 325 | "skeleton/lib/helper" 326 | 327 | "google.golang.org/grpc/codes" 328 | "google.golang.org/grpc/status" 329 | ) 330 | 331 | func (u *repo) Find(ctx context.Context, id string) error { 332 | select { 333 | case <-ctx.Done(): 334 | return helper.ContextError(ctx) 335 | default: 336 | } 337 | 338 | query := ` 339 | SELECT id, name, phone, licence_number, company_id, company_name 340 | FROM drivers WHERE id = $1 AND is_deleted = false 341 | ` 342 | 343 | err := u.db.QueryRowContext(ctx, query, id).Scan( 344 | &u.pb.Id, &u.pb.Name, &u.pb.LicenceNumber, &u.pb.CompanyId, &u.pb.CompanyName) 345 | 346 | if err != nil { 347 | u.log.Println(err.Error()) 348 | return status.Error(codes.Internal, err.Error()) 349 | } 350 | 351 | return nil 352 | } 353 | ``` 354 | 355 | * Buat file domain/ddrivers/repositories/create\_driver.go 356 | 357 | ```go 358 | package repositories 359 | 360 | import ( 361 | "context" 362 | "skeleton/lib/helper" 363 | "time" 364 | 365 | "github.com/google/uuid" 366 | "google.golang.org/grpc/codes" 367 | "google.golang.org/grpc/status" 368 | ) 369 | 370 | func (u *repo) Create(ctx context.Context) error { 371 | select { 372 | case <-ctx.Done(): 373 | return helper.ContextError(ctx) 374 | default: 375 | } 376 | 377 | query := ` 378 | INSERT INTO drivers ( 379 | id, name, phone, licence_number, company_id, company_name, created, created_by, updated, updated_by) 380 | VALUES ($1, $2, $3 ,$4, $5, $6, $7, $8, $9, $10) 381 | ` 382 | u.pb.Id = uuid.New().String() 383 | now := time.Now().Format("2006-01-02 15:04:05.000000") 384 | _, err := u.db.ExecContext(ctx, query, 385 | u.pb.Id, u.pb.Name, u.pb.Phone, u.pb.LicenceNumber, u.pb.CompanyId, u.pb.CompanyName, 386 | now, u.pb.CreatedBy, now, u.pb.UpdatedBy) 387 | 388 | if err != nil { 389 | u.log.Println(err.Error()) 390 | return status.Error(codes.Internal, err.Error()) 391 | } 392 | 393 | return nil 394 | } 395 | ``` 396 | 397 | * Buat file domain/ddrivers/repositories/update\_driver\_by\_id.go 398 | 399 | ```go 400 | package repositories 401 | 402 | import ( 403 | "context" 404 | "skeleton/lib/helper" 405 | "time" 406 | 407 | "google.golang.org/grpc/codes" 408 | "google.golang.org/grpc/status" 409 | ) 410 | 411 | func (u *repo) Update(ctx context.Context) error { 412 | select { 413 | case <-ctx.Done(): 414 | return helper.ContextError(ctx) 415 | default: 416 | } 417 | 418 | query := ` 419 | UPDATE drivers 420 | SET name = $1, 421 | phone = $2, 422 | licence_number = $3, 423 | updated = $4, 424 | updated_by = $5 425 | WHERE id = $6 426 | ` 427 | now := time.Now().Format("2006-01-02 15:04:05.000000") 428 | _, err := u.db.ExecContext(ctx, query, 429 | u.pb.Name, u.pb.Phone, u.pb.LicenceNumber, now, u.pb.UpdatedBy, u.pb.Id) 430 | 431 | if err != nil { 432 | u.log.Println(err.Error()) 433 | return status.Error(codes.Internal, err.Error()) 434 | } 435 | 436 | return nil 437 | } 438 | ``` 439 | 440 | * Buat file domain/ddrivers/repositories/delete\_driver\_by\_id.go 441 | 442 | ```go 443 | package repositories 444 | 445 | import ( 446 | "context" 447 | "skeleton/lib/helper" 448 | "skeleton/pb/generic" 449 | 450 | "google.golang.org/grpc/codes" 451 | "google.golang.org/grpc/status" 452 | ) 453 | 454 | func (u *repo) Delete(ctx context.Context, in *generic.Id) error { 455 | select { 456 | case <-ctx.Done(): 457 | return helper.ContextError(ctx) 458 | default: 459 | } 460 | 461 | query := ` 462 | UPDATE drivers 463 | SET is_deleted = true 464 | WHERE id = $1 465 | ` 466 | _, err := u.db.ExecContext(ctx, query, in.Id) 467 | 468 | if err != nil { 469 | u.log.Println(err.Error()) 470 | return status.Error(codes.Internal, err.Error()) 471 | } 472 | 473 | return nil 474 | } 475 | ``` 476 | 477 | ## Validation 478 | 479 | * Setiap request perlu divalidasi 480 | * Buat file domain/ddrivers/validation\_interface.go 481 | 482 | ```go 483 | package ddrivers 484 | 485 | import ( 486 | "context" 487 | "skeleton/pb/drivers" 488 | ) 489 | 490 | type DriverValidationInterface interface { 491 | Create(ctx context.Context, id *drivers.Driver) error 492 | Update(ctx context.Context, id *drivers.Driver) error 493 | Delete(ctx context.Context, id string) error 494 | } 495 | ``` 496 | 497 | * Untuk kemudahan pengelolaan kode, semua implementasi validasi akan dimasukkan dalam folder domain/drivers/validation 498 | * Buat file domain/ddrivers/validation/driover\_validation.go 499 | 500 | ```go 501 | package validation 502 | 503 | import ( 504 | "log" 505 | "skeleton/domain/ddrivers" 506 | ) 507 | 508 | type driverValidation struct { 509 | log *log.Logger 510 | driverRepo ddrivers.DriverRepoInterface 511 | } 512 | 513 | func NewValidation(log *log.Logger, driverRepo ddrivers.DriverRepoInterface) ddrivers.DriverValidationInterface { 514 | return &driverValidation{ 515 | log: log, 516 | driverRepo: driverRepo, 517 | } 518 | } 519 | ``` 520 | 521 | * Buat file domain/ddrivers/validation/create\_driver\_validation.go 522 | 523 | ```go 524 | package validation 525 | 526 | import ( 527 | "context" 528 | "skeleton/lib/helper" 529 | "skeleton/pb/drivers" 530 | 531 | "google.golang.org/grpc/codes" 532 | "google.golang.org/grpc/status" 533 | ) 534 | 535 | func (u *driverValidation) Create(ctx context.Context, in *drivers.Driver) error { 536 | select { 537 | case <-ctx.Done(): 538 | return helper.ContextError(ctx) 539 | default: 540 | } 541 | 542 | if len(in.Name) == 0 { 543 | u.log.Println("please supply valid name") 544 | return status.Error(codes.InvalidArgument, "please supply valid name") 545 | } 546 | 547 | if len(in.Phone) == 0 { 548 | u.log.Println("please supply valid phone") 549 | return status.Error(codes.InvalidArgument, "please supply valid phone") 550 | } 551 | 552 | if len(in.CompanyId) == 0 { 553 | u.log.Println("please supply valid company id") 554 | return status.Error(codes.InvalidArgument, "please supply valid company id") 555 | } 556 | 557 | if len(in.CompanyName) == 0 { 558 | u.log.Println("please supply valid company name") 559 | return status.Error(codes.InvalidArgument, "please supply valid company name") 560 | } 561 | 562 | if len(in.LicenceNumber) == 0 { 563 | u.log.Println("please supply valid licence number") 564 | return status.Error(codes.InvalidArgument, "please supply valid licence number") 565 | } 566 | 567 | return nil 568 | } 569 | ``` 570 | 571 | * Buat file domain/ddrivers/validation/update\_driver\_validation.go 572 | 573 | ```go 574 | package validation 575 | 576 | import ( 577 | "context" 578 | "skeleton/lib/helper" 579 | "skeleton/pb/drivers" 580 | 581 | "google.golang.org/grpc/codes" 582 | "google.golang.org/grpc/status" 583 | ) 584 | 585 | func (u *driverValidation) Create(ctx context.Context, in *drivers.Driver) error { 586 | select { 587 | case <-ctx.Done(): 588 | return helper.ContextError(ctx) 589 | default: 590 | } 591 | 592 | if len(in.Name) == 0 { 593 | u.log.Println("please supply valid name") 594 | return status.Error(codes.InvalidArgument, "please supply valid name") 595 | } 596 | 597 | if len(in.Phone) == 0 { 598 | u.log.Println("please supply valid phone") 599 | return status.Error(codes.InvalidArgument, "please supply valid phone") 600 | } 601 | 602 | if len(in.CompanyId) == 0 { 603 | u.log.Println("please supply valid company id") 604 | return status.Error(codes.InvalidArgument, "please supply valid company id") 605 | } 606 | 607 | if len(in.CompanyName) == 0 { 608 | u.log.Println("please supply valid company name") 609 | return status.Error(codes.InvalidArgument, "please supply valid company name") 610 | } 611 | 612 | if len(in.LicenceNumber) == 0 { 613 | u.log.Println("please supply valid licence number") 614 | return status.Error(codes.InvalidArgument, "please supply valid licence number") 615 | } 616 | 617 | return nil 618 | } 619 | ``` 620 | 621 | * Buat file domain/ddrivers/validation/delete\_driver/validation.fo 622 | 623 | ```go 624 | package validation 625 | 626 | import ( 627 | "context" 628 | "skeleton/lib/helper" 629 | ) 630 | 631 | func (u *driverValidation) Delete(ctx context.Context, id string) error { 632 | select { 633 | case <-ctx.Done(): 634 | return helper.ContextError(ctx) 635 | default: 636 | } 637 | 638 | err := u.driverRepo.Find(ctx, id) 639 | if err != nil { 640 | return err 641 | } 642 | 643 | return nil 644 | } 645 | ``` 646 | 647 | ## Usecase 648 | 649 | * usacase digunakan untuk menghandle logic. usecase yang akan memanggil validation maupun repository sekiranya logic membutuhkan hal tersebut. 650 | * Buat file usecase\_interface.go 651 | 652 | ```go 653 | package ddrivers 654 | 655 | import ( 656 | "context" 657 | "skeleton/pb/drivers" 658 | "skeleton/pb/generic" 659 | ) 660 | 661 | type DriverUsecaseInterface interface { 662 | List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) 663 | Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) 664 | Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) 665 | Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) 666 | } 667 | ``` 668 | 669 | * Buat folder usecase untuk menyimpan seluruh fiule implementasi usecase interface. 670 | * Buat file domain/ddrivers/usecase/usecase.go 671 | 672 | ```go 673 | package usecase 674 | 675 | import ( 676 | "log" 677 | "skeleton/domain/ddrivers" 678 | ) 679 | 680 | type service struct { 681 | log *log.Logger 682 | driverRepo ddrivers.DriverRepoInterface 683 | } 684 | 685 | func NewService(log *log.Logger, driverRepo ddrivers.DriverRepoInterface) ddrivers.DriverUsecaseInterface { 686 | return &service{ 687 | log: log, 688 | driverRepo: driverRepo, 689 | } 690 | } 691 | ``` 692 | 693 | * Buat file domain/ddrivers/usecase/create\_driver\_usecase.go 694 | 695 | ```go 696 | package usecase 697 | 698 | import ( 699 | "context" 700 | "skeleton/domain/ddrivers/validation" 701 | "skeleton/lib/helper" 702 | "skeleton/pb/drivers" 703 | ) 704 | 705 | func (u *service) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 706 | select { 707 | case <-ctx.Done(): 708 | return nil, helper.ContextError(ctx) 709 | default: 710 | } 711 | 712 | dValidation := validation.NewValidation(u.log, u.driverRepo) 713 | err := dValidation.Create(ctx, in) 714 | if err != nil { 715 | return nil, err 716 | } 717 | 718 | u.driverRepo.SetPb(in) 719 | 720 | err = u.driverRepo.Create(ctx) 721 | if err != nil { 722 | return nil, err 723 | } 724 | 725 | return u.driverRepo.GetPb(), nil 726 | } 727 | ``` 728 | 729 | * Buat file domain/ddrivers/usecase/list\_driver\_usecase.go 730 | 731 | ```go 732 | package usecase 733 | 734 | import ( 735 | "context" 736 | "skeleton/lib/helper" 737 | "skeleton/pb/drivers" 738 | ) 739 | 740 | func (u *service) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 741 | select { 742 | case <-ctx.Done(): 743 | return nil, helper.ContextError(ctx) 744 | default: 745 | } 746 | 747 | return u.driverRepo.FindAll(ctx, in) 748 | } 749 | ``` 750 | 751 | * Buat file domain/ddrivers/usecase/update\_driver\_usecase.go 752 | 753 | ```go 754 | package usecase 755 | 756 | import ( 757 | "context" 758 | "skeleton/domain/ddrivers/validation" 759 | "skeleton/lib/helper" 760 | "skeleton/pb/drivers" 761 | ) 762 | 763 | func (u *service) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 764 | select { 765 | case <-ctx.Done(): 766 | return nil, helper.ContextError(ctx) 767 | default: 768 | } 769 | 770 | dValidation := validation.NewValidation(u.log, u.driverRepo) 771 | err := dValidation.Update(ctx, in) 772 | if err != nil { 773 | return nil, err 774 | } 775 | 776 | u.driverRepo.SetPb(in) 777 | 778 | err = u.driverRepo.Update(ctx) 779 | if err != nil { 780 | return nil, err 781 | } 782 | 783 | return u.driverRepo.GetPb(), nil 784 | } 785 | ``` 786 | 787 | * Buat file domain.ddrivers/usecase/delete\_driver\_usecase.go 788 | 789 | ```go 790 | package usecase 791 | 792 | import ( 793 | "context" 794 | "skeleton/domain/ddrivers/validation" 795 | "skeleton/lib/helper" 796 | "skeleton/pb/generic" 797 | ) 798 | 799 | func (u *service) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 800 | select { 801 | case <-ctx.Done(): 802 | return nil, helper.ContextError(ctx) 803 | default: 804 | } 805 | 806 | dValidation := validation.NewValidation(u.log, u.driverRepo) 807 | err := dValidation.Delete(ctx, in.Id) 808 | if err != nil { 809 | return nil, err 810 | } 811 | 812 | err = u.driverRepo.Delete(ctx, in) 813 | if err != nil { 814 | return nil, err 815 | } 816 | 817 | return &generic.BoolMessage{IsTrue: true}, nil 818 | } 819 | ``` 820 | 821 | ## Perbarui Handler 822 | 823 | * handler merupakan endpoint service. 824 | * handler mengimpolementasikan seluruh seluruh funhgsi dari interface DomainServiceServer 825 | * Buat file domain/ddrivers/handler/grpc.go 826 | 827 | ```go 828 | package handler 829 | 830 | import ( 831 | "context" 832 | "skeleton/domain/ddrivers" 833 | "skeleton/pb/drivers" 834 | "skeleton/pb/generic" 835 | ) 836 | 837 | type DriverHandler struct { 838 | usecase ddrivers.DriverUsecaseInterface 839 | } 840 | 841 | func NewDriverHandler(usecase ddrivers.DriverUsecaseInterface) *DriverHandler { 842 | handler := new(DriverHandler) 843 | handler.usecase = usecase 844 | return handler 845 | } 846 | 847 | func (u *DriverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 848 | return u.usecase.List(ctx, in) 849 | } 850 | 851 | func (u *DriverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 852 | return u.usecase.Create(ctx, in) 853 | } 854 | 855 | func (u *DriverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 856 | return u.usecase.Update(ctx, in) 857 | } 858 | 859 | func (u *DriverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 860 | return u.usecase.Delete(ctx, in) 861 | } 862 | ``` 863 | 864 | ## Perbarui Routing 865 | 866 | * Ubah file route/route.go 867 | 868 | ```go 869 | package route 870 | 871 | import ( 872 | "database/sql" 873 | "log" 874 | 875 | driverHandler "skeleton/domain/ddrivers/handler" 876 | driverRepo "skeleton/domain/ddrivers/repositories" 877 | driverUsecase "skeleton/domain/ddrivers/usecase" 878 | "skeleton/pb/drivers" 879 | 880 | "google.golang.org/grpc" 881 | ) 882 | 883 | func GrpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB) { 884 | driverServer := driverHandler.NewDriverHandler( 885 | driverUsecase.NewService(log, driverRepo.NewDriverRepo(db, log)), 886 | ) 887 | 888 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 889 | } 890 | ``` 891 | 892 | -------------------------------------------------------------------------------- /grpc-framework/grpc-database.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | * Kita sudah pernah mempelajari [database](../build-rest-api-framework/database.md) 4 | * Update .env untuk menambahkan konfigurasi database 5 | 6 | ```text 7 | PORT = 7070 8 | POSTGRES_HOST = localhost 9 | POSTGRES_PORT = 5432 10 | POSTGRES_USER = postgres 11 | POSTGRES_PASSWORD = pass 12 | POSTGRES_DB = drivers 13 | ``` 14 | 15 | * Buat file lib/database/postgres/postgres.go 16 | 17 | ```go 18 | package postgres 19 | 20 | import ( 21 | "context" 22 | "database/sql" 23 | "fmt" 24 | "os" 25 | "strconv" 26 | ) 27 | 28 | // Open database commection 29 | func Open() (*sql.DB, error) { 30 | var db *sql.DB 31 | port, err := strconv.Atoi(os.Getenv("POSTGRES_PORT")) 32 | if err != nil { 33 | return db, err 34 | } 35 | 36 | return sql.Open("postgres", 37 | fmt.Sprintf( 38 | "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", 39 | os.Getenv("POSTGRES_HOST"), port, os.Getenv("POSTGRES_USER"), 40 | os.Getenv("POSTGRES_PASSWORD"), os.Getenv("POSTGRES_DB"), 41 | ), 42 | ) 43 | } 44 | 45 | // StatusCheck returns nil if it can successfully talk to the database. It 46 | // returns a non-nil error otherwise. 47 | func StatusCheck(ctx context.Context, db *sql.DB) error { 48 | 49 | // Run a simple query to determine connectivity. The db has a "Ping" method 50 | // but it can false-positive when it was previously able to talk to the 51 | // database but the database has since gone away. Running this query forces a 52 | // round trip to the database. 53 | const q = `SELECT true` 54 | var tmp bool 55 | return db.QueryRowContext(ctx, q).Scan(&tmp) 56 | } 57 | ``` 58 | 59 | * Buat file schema/migrate.go 60 | 61 | ```go 62 | package schema 63 | 64 | import ( 65 | "database/sql" 66 | 67 | "github.com/GuiaBolso/darwin" 68 | ) 69 | 70 | var migrations = []darwin.Migration{ 71 | { 72 | Version: 1, 73 | Description: "Create drivers Table", 74 | Script: ` 75 | CREATE TABLE public.drivers ( 76 | id uuid NOT NULL, 77 | name varchar NOT NULL, 78 | phone varchar NOT NULL, 79 | licence_number varchar NOT NULL, 80 | company_id varchar NOT NULL, 81 | company_name varchar NOT NULL, 82 | is_deleted bool NOT NULL DEFAULT false, 83 | created timestamp(0) NOT NULL, 84 | created_by varchar NOT NULL, 85 | updated timestamp(0) NOT NULL, 86 | updated_by varchar NOT NULL, 87 | CONSTRAINT drivers_pk PRIMARY KEY (id) 88 | ); 89 | CREATE UNIQUE INDEX drivers_phone ON public.drivers USING btree (phone); 90 | `, 91 | }, 92 | } 93 | 94 | // Migrate attempts to bring the schema for db up to date with the migrations 95 | // defined in this package. 96 | func Migrate(db *sql.DB) error { 97 | driver := darwin.NewGenericDriver(db, darwin.PostgresDialect{}) 98 | 99 | d := darwin.New(driver, migrations, nil) 100 | 101 | return d.Migrate() 102 | } 103 | ``` 104 | 105 | * Buat file schema/seed.go 106 | 107 | ```go 108 | package schema 109 | 110 | import ( 111 | "database/sql" 112 | "fmt" 113 | ) 114 | 115 | // seeds is a string constant containing all of the queries needed to get the 116 | // db seeded to a useful state for development. 117 | // 118 | // Using a constant in a .go file is an easy way to ensure the queries are part 119 | // of the compiled executable and avoids pathing issues with the working 120 | // directory. It has the downside that it lacks syntax highlighting and may be 121 | // harder to read for some cases compared to using .sql files. You may also 122 | // consider a combined approach using a tool like packr or go-bindata. 123 | // 124 | // Note that database servers besides PostgreSQL may not support running 125 | // multiple queries as part of the same execution so this single large constant 126 | // may need to be broken up. 127 | 128 | // Seed runs the set of seed-data queries against db. The queries are ran in a 129 | // transaction and rolled back if any fail. 130 | func Seed(db *sql.DB, seeds ...string) error { 131 | 132 | tx, err := db.Begin() 133 | if err != nil { 134 | return err 135 | } 136 | 137 | for _, seed := range seeds { 138 | _, err = tx.Exec(seed) 139 | if err != nil { 140 | tx.Rollback() 141 | fmt.Println("error execute seed") 142 | return err 143 | } 144 | } 145 | 146 | return tx.Commit() 147 | } 148 | ``` 149 | 150 | * Buat file cmd/cli.go 151 | 152 | ```go 153 | package main 154 | 155 | import ( 156 | "flag" 157 | "fmt" 158 | "log" 159 | "os" 160 | 161 | "skeleton/config" 162 | "skeleton/lib/database/postgres" 163 | "skeleton/schema" 164 | 165 | _ "github.com/lib/pq" 166 | ) 167 | 168 | func main() { 169 | config.Setup(".env") 170 | 171 | log := log.New(os.Stdout, "Skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 172 | if err := run(log); err != nil { 173 | log.Printf("error: shutting down: %s", err) 174 | os.Exit(1) 175 | } 176 | } 177 | 178 | func run(log *log.Logger) error { 179 | // ========================================================================= 180 | // App Starting 181 | 182 | log.Printf("main : Started") 183 | defer log.Println("main : Completed") 184 | 185 | // ========================================================================= 186 | 187 | // Start Database 188 | 189 | db, err := postgres.Open() 190 | if err != nil { 191 | return fmt.Errorf("connecting to db: %v", err) 192 | } 193 | defer db.Close() 194 | 195 | // Handle cli command 196 | flag.Parse() 197 | 198 | switch flag.Arg(0) { 199 | case "migrate": 200 | if err := schema.Migrate(db); err != nil { 201 | return fmt.Errorf("applying migrations: %v", err) 202 | } 203 | log.Println("Migrations complete") 204 | return nil 205 | 206 | case "seed": 207 | if err := schema.Seed(db); err != nil { 208 | return fmt.Errorf("seeding database: %v", err) 209 | } 210 | log.Println("Seed data complete") 211 | return nil 212 | } 213 | 214 | return nil 215 | } 216 | ``` 217 | 218 | * Buat database drivers 219 | * Jalankan `go run cmd/cli.go migrate` 220 | * Update file server.go untuk membuat koneksi database 221 | 222 | ```go 223 | package main 224 | 225 | import ( 226 | "context" 227 | "database/sql" 228 | "log" 229 | "net" 230 | "os" 231 | "skeleton/config" 232 | "skeleton/lib/database/postgres" 233 | "skeleton/pb/drivers" 234 | "skeleton/pb/generic" 235 | 236 | _ "github.com/lib/pq" 237 | "google.golang.org/grpc" 238 | ) 239 | 240 | func main() { 241 | config.Setup(".env") 242 | 243 | log := log.New(os.Stdout, "Skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 244 | 245 | db, err := postgres.Open() 246 | if err != nil { 247 | log.Fatalf("connecting to db: %v", err) 248 | return 249 | } 250 | log.Print("connecting to postgresql database") 251 | 252 | defer db.Close() 253 | 254 | // listen tcp port 255 | lis, err := net.Listen("tcp", ":"+os.Getenv("PORT")) 256 | if err != nil { 257 | log.Fatalf("failed to listen: %v", err) 258 | return 259 | } 260 | 261 | grpcServer := grpc.NewServer() 262 | 263 | // routing grpc services 264 | grpcRoute(grpcServer, log, db) 265 | 266 | if err := grpcServer.Serve(lis); err != nil { 267 | log.Fatalf("failed to serve: %s", err) 268 | return 269 | } 270 | log.Print("serve grpc on port: " + os.Getenv("PORT")) 271 | 272 | } 273 | ``` 274 | 275 | * Update server.go untuk mengaupdate roting dengan menginject db ke service 276 | 277 | ```go 278 | func grpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB) { 279 | driverServer := newDriverHandler(log, db) 280 | 281 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 282 | } 283 | ``` 284 | 285 | * Update server.go service handler agar mempunyai proprety db 286 | 287 | ```go 288 | type driverHandler struct { 289 | log *log.Logger 290 | db *sql.DB 291 | } 292 | 293 | func newDriverHandler(log *log.Logger, db *sql.DB) *driverHandler { 294 | handler := new(driverHandler) 295 | handler.log = log 296 | handler.db = db 297 | return handler 298 | } 299 | ``` 300 | 301 | * Update file server.go untuk membuat fungsi logError 302 | 303 | ```go 304 | func logError(log *log.Logger, code codes.Code, err error) error { 305 | log.Print(err.Error()) 306 | return status.Error(code, err.Error()) 307 | } 308 | ``` 309 | 310 | * Update file server.go untuk mengupdate fungsi List 311 | 312 | ```go 313 | func (u *driverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 314 | out := &drivers.Drivers{} 315 | query := `SELECT id, name, phone, licence_number, company_id, company_name FROM drivers` 316 | where := []string{"is_deleted = false"} 317 | paramQueries := []interface{}{} 318 | 319 | if len(in.Ids) > 0 { 320 | orWhere := []string{} 321 | for _, id := range in.Ids { 322 | paramQueries = append(paramQueries, id) 323 | orWhere = append(orWhere, fmt.Sprintf("id = %d", len(paramQueries))) 324 | } 325 | if len(orWhere) > 0 { 326 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 327 | } 328 | } 329 | 330 | if len(in.CompanyIds) > 0 { 331 | orWhere := []string{} 332 | for _, id := range in.CompanyIds { 333 | paramQueries = append(paramQueries, id) 334 | orWhere = append(orWhere, fmt.Sprintf("company_id = %d", len(paramQueries))) 335 | } 336 | if len(orWhere) > 0 { 337 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 338 | } 339 | } 340 | 341 | if len(in.LicenceNumbers) > 0 { 342 | orWhere := []string{} 343 | for _, licenceNumber := range in.LicenceNumbers { 344 | paramQueries = append(paramQueries, licenceNumber) 345 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 346 | } 347 | if len(orWhere) > 0 { 348 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 349 | } 350 | } 351 | 352 | if len(in.Names) > 0 { 353 | orWhere := []string{} 354 | for _, name := range in.Names { 355 | paramQueries = append(paramQueries, name) 356 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 357 | } 358 | if len(orWhere) > 0 { 359 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 360 | } 361 | } 362 | 363 | if len(in.Phones) > 0 { 364 | orWhere := []string{} 365 | for _, phone := range in.Phones { 366 | paramQueries = append(paramQueries, phone) 367 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 368 | } 369 | if len(orWhere) > 0 { 370 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 371 | } 372 | } 373 | 374 | if in.Pagination == nil { 375 | in.Pagination = &generic.Pagination{} 376 | } 377 | 378 | if len(in.Pagination.Keyword) > 0 { 379 | orWhere := []string{} 380 | 381 | paramQueries = append(paramQueries, in.Pagination.Keyword) 382 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 383 | 384 | paramQueries = append(paramQueries, in.Pagination.Keyword) 385 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 386 | 387 | paramQueries = append(paramQueries, in.Pagination.Keyword) 388 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 389 | 390 | paramQueries = append(paramQueries, in.Pagination.Keyword) 391 | orWhere = append(orWhere, fmt.Sprintf("company_name = %d", len(paramQueries))) 392 | 393 | if len(orWhere) > 0 { 394 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 395 | } 396 | } 397 | 398 | if len(in.Pagination.Sort) > 0 { 399 | in.Pagination.Sort = strings.ToLower(in.Pagination.Sort) 400 | if in.Pagination.Sort != "asc" { 401 | in.Pagination.Sort = "desc" 402 | } 403 | } else { 404 | in.Pagination.Sort = "desc" 405 | } 406 | 407 | if len(in.Pagination.Order) > 0 { 408 | in.Pagination.Order = strings.ToLower(in.Pagination.Order) 409 | if !(in.Pagination.Order == "id" || 410 | in.Pagination.Order == "name" || 411 | in.Pagination.Order == "phone" || 412 | in.Pagination.Order == "licence_number" || 413 | in.Pagination.Order == "company_id" || 414 | in.Pagination.Order == "company_name") { 415 | in.Pagination.Order = "id" 416 | } 417 | } else { 418 | in.Pagination.Order = "id" 419 | } 420 | 421 | if in.Pagination.Limit <= 0 { 422 | in.Pagination.Limit = 10 423 | } 424 | 425 | if in.Pagination.Offset <= 0 { 426 | in.Pagination.Offset = 0 427 | } 428 | 429 | if len(where) > 0 { 430 | query += " WHERE " + strings.Join(where, " AND ") 431 | } 432 | 433 | query += " ORDER BY " + in.Pagination.Order + " " + in.Pagination.Sort 434 | query += " LIMIT " + strconv.Itoa(int(in.Pagination.Limit)) 435 | query += " OFFSET " + strconv.Itoa(int(in.Pagination.Offset)) 436 | 437 | rows, err := u.db.QueryContext(ctx, query, paramQueries...) 438 | if err != nil { 439 | return out, logError(u.log, codes.Internal, err) 440 | } 441 | defer rows.Close() 442 | 443 | for rows.Next() { 444 | var obj drivers.Driver 445 | err = rows.Scan(&obj.Id, &obj.Name, &obj.Phone, &obj.LicenceNumber, &obj.CompanyId, &obj.CompanyName) 446 | if err != nil { 447 | return out, logError(u.log, codes.Internal, err) 448 | } 449 | 450 | out.Driver = append(out.Driver, &obj) 451 | } 452 | 453 | if rows.Err() != nil { 454 | return out, logError(u.log, codes.Internal, rows.Err()) 455 | } 456 | 457 | return out, nil 458 | } 459 | ``` 460 | 461 | * Update file server.go untuk mengupdate fungsi Create 462 | 463 | ```go 464 | func (u *driverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 465 | query := ` 466 | INSERT INTO drivers ( 467 | id, name, phone, licence_number, company_id, company_name, created, created_by, updated, updated_by) 468 | VALUES ($1, $2, $3 ,$4, $5, $6, $7, $8, $9, $10) 469 | ` 470 | in.Id = uuid.New().String() 471 | now := time.Now().Format("2006-01-02 15:04:05.000000") 472 | _, err := u.db.ExecContext(ctx, query, 473 | in.Id, in.Name, in.Phone, in.LicenceNumber, in.CompanyId, in.CompanyName, now, "jaka", now, "jaka") 474 | 475 | if err != nil { 476 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 477 | } 478 | 479 | return in, nil 480 | } 481 | ``` 482 | 483 | * Update file server.go untuk mengupdate fungsi Update 484 | 485 | ```go 486 | func (u *driverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 487 | query := ` 488 | UPDATE drivers 489 | SET name = $1, 490 | phone = $2, 491 | licence_number = $3, 492 | updated = $4, 493 | updated_by = $5 494 | WHERE id = $6 495 | ` 496 | now := time.Now().Format("2006-01-02 15:04:05.000000") 497 | _, err := u.db.ExecContext(ctx, query, 498 | in.Name, in.Phone, in.LicenceNumber, now, "jaka", in.Id) 499 | 500 | if err != nil { 501 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 502 | } 503 | 504 | return in, nil 505 | } 506 | ``` 507 | 508 | * Update file server.go untuk mengupdate fungsi Delete 509 | 510 | ```go 511 | func (u *driverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 512 | query := ` 513 | UPDATE drivers 514 | SET is_deleted = true 515 | WHERE id = $1 516 | ` 517 | _, err := u.db.ExecContext(ctx, query, in.Id) 518 | 519 | if err != nil { 520 | return &generic.BoolMessage{IsTrue: false}, logError(u.log, codes.Internal, err) 521 | } 522 | 523 | return &generic.BoolMessage{IsTrue: true}, nil 524 | } 525 | ``` 526 | 527 | * Test create dengan perintah `grpcurl -plaintext -import-path ~/jackyhtg/skeleton/proto -proto ~/jackyhtg/skeleton/proto/drivers/driver_service.proto -d '{"name": "jacky", "phone": "08172221", "licence_number": "1234", "company_id": "UAT", "company_name": "Universal Alabama Tahoma"}' localhost:7070 skeleton.DriversService.Create` 528 | * Tes list dengan perintah `grpcurl -import-path ~/jackyhtg/skeleton/proto -proto ~/jackyhtg/skeleton/proto/drivers/driver_service.proto -plaintext localhost:7070 skeleton.DriversService.List` 529 | * Tes delete denagn perintah `grpcurl -plaintext -import-path ~/jackyhtg/skeleton/proto -proto ~/jackyhtg/skeleton/proto/drivers/driver_service.proto -d '{"id":"3a36a71f-021c-4465-9fda-36699b320855"}' localhost:7070 skeleton.DriversService.Delete` 530 | 531 | Ini adalah kode keseluruhan server.go 532 | 533 | ```go 534 | package main 535 | 536 | import ( 537 | "context" 538 | "database/sql" 539 | "fmt" 540 | "log" 541 | "net" 542 | "os" 543 | "strconv" 544 | "strings" 545 | "time" 546 | 547 | "skeleton/config" 548 | "skeleton/lib/database/postgres" 549 | "skeleton/pb/drivers" 550 | "skeleton/pb/generic" 551 | 552 | "github.com/google/uuid" 553 | _ "github.com/lib/pq" 554 | "google.golang.org/grpc" 555 | "google.golang.org/grpc/codes" 556 | "google.golang.org/grpc/status" 557 | ) 558 | 559 | func main() { 560 | config.Setup(".env") 561 | 562 | log := log.New(os.Stdout, "Skeleton : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) 563 | 564 | db, err := postgres.Open() 565 | if err != nil { 566 | log.Fatalf("connecting to db: %v", err) 567 | return 568 | } 569 | log.Print("connecting to postgresql database") 570 | 571 | defer db.Close() 572 | 573 | // listen tcp port 574 | lis, err := net.Listen("tcp", ":"+os.Getenv("PORT")) 575 | if err != nil { 576 | log.Fatalf("failed to listen: %v", err) 577 | return 578 | } 579 | 580 | grpcServer := grpc.NewServer() 581 | 582 | // routing grpc services 583 | grpcRoute(grpcServer, log, db) 584 | 585 | if err := grpcServer.Serve(lis); err != nil { 586 | log.Fatalf("failed to serve: %s", err) 587 | return 588 | } 589 | log.Print("serve grpc on port: " + os.Getenv("PORT")) 590 | 591 | } 592 | 593 | func grpcRoute(grpcServer *grpc.Server, log *log.Logger, db *sql.DB) { 594 | driverServer := newDriverHandler(log, db) 595 | 596 | drivers.RegisterDriversServiceServer(grpcServer, driverServer) 597 | } 598 | 599 | type driverHandler struct { 600 | log *log.Logger 601 | db *sql.DB 602 | } 603 | 604 | func newDriverHandler(log *log.Logger, db *sql.DB) *driverHandler { 605 | handler := new(driverHandler) 606 | handler.log = log 607 | handler.db = db 608 | return handler 609 | } 610 | 611 | func (u *driverHandler) List(ctx context.Context, in *drivers.DriverListInput) (*drivers.Drivers, error) { 612 | out := &drivers.Drivers{} 613 | query := `SELECT id, name, phone, licence_number, company_id, company_name FROM drivers` 614 | where := []string{"is_deleted = false"} 615 | paramQueries := []interface{}{} 616 | 617 | if len(in.Ids) > 0 { 618 | orWhere := []string{} 619 | for _, id := range in.Ids { 620 | paramQueries = append(paramQueries, id) 621 | orWhere = append(orWhere, fmt.Sprintf("id = %d", len(paramQueries))) 622 | } 623 | if len(orWhere) > 0 { 624 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 625 | } 626 | } 627 | 628 | if len(in.CompanyIds) > 0 { 629 | orWhere := []string{} 630 | for _, id := range in.CompanyIds { 631 | paramQueries = append(paramQueries, id) 632 | orWhere = append(orWhere, fmt.Sprintf("company_id = %d", len(paramQueries))) 633 | } 634 | if len(orWhere) > 0 { 635 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 636 | } 637 | } 638 | 639 | if len(in.LicenceNumbers) > 0 { 640 | orWhere := []string{} 641 | for _, licenceNumber := range in.LicenceNumbers { 642 | paramQueries = append(paramQueries, licenceNumber) 643 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 644 | } 645 | if len(orWhere) > 0 { 646 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 647 | } 648 | } 649 | 650 | if len(in.Names) > 0 { 651 | orWhere := []string{} 652 | for _, name := range in.Names { 653 | paramQueries = append(paramQueries, name) 654 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 655 | } 656 | if len(orWhere) > 0 { 657 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 658 | } 659 | } 660 | 661 | if len(in.Phones) > 0 { 662 | orWhere := []string{} 663 | for _, phone := range in.Phones { 664 | paramQueries = append(paramQueries, phone) 665 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 666 | } 667 | if len(orWhere) > 0 { 668 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 669 | } 670 | } 671 | 672 | if in.Pagination == nil { 673 | in.Pagination = &generic.Pagination{} 674 | } 675 | 676 | if len(in.Pagination.Keyword) > 0 { 677 | orWhere := []string{} 678 | 679 | paramQueries = append(paramQueries, in.Pagination.Keyword) 680 | orWhere = append(orWhere, fmt.Sprintf("name = %d", len(paramQueries))) 681 | 682 | paramQueries = append(paramQueries, in.Pagination.Keyword) 683 | orWhere = append(orWhere, fmt.Sprintf("phone = %d", len(paramQueries))) 684 | 685 | paramQueries = append(paramQueries, in.Pagination.Keyword) 686 | orWhere = append(orWhere, fmt.Sprintf("licence_number = %d", len(paramQueries))) 687 | 688 | paramQueries = append(paramQueries, in.Pagination.Keyword) 689 | orWhere = append(orWhere, fmt.Sprintf("company_name = %d", len(paramQueries))) 690 | 691 | if len(orWhere) > 0 { 692 | where = append(where, "("+strings.Join(orWhere, " OR ")+")") 693 | } 694 | } 695 | 696 | if len(in.Pagination.Sort) > 0 { 697 | in.Pagination.Sort = strings.ToLower(in.Pagination.Sort) 698 | if in.Pagination.Sort != "asc" { 699 | in.Pagination.Sort = "desc" 700 | } 701 | } else { 702 | in.Pagination.Sort = "desc" 703 | } 704 | 705 | if len(in.Pagination.Order) > 0 { 706 | in.Pagination.Order = strings.ToLower(in.Pagination.Order) 707 | if !(in.Pagination.Order == "id" || 708 | in.Pagination.Order == "name" || 709 | in.Pagination.Order == "phone" || 710 | in.Pagination.Order == "licence_number" || 711 | in.Pagination.Order == "company_id" || 712 | in.Pagination.Order == "company_name") { 713 | in.Pagination.Order = "id" 714 | } 715 | } else { 716 | in.Pagination.Order = "id" 717 | } 718 | 719 | if in.Pagination.Limit <= 0 { 720 | in.Pagination.Limit = 10 721 | } 722 | 723 | if in.Pagination.Offset <= 0 { 724 | in.Pagination.Offset = 0 725 | } 726 | 727 | if len(where) > 0 { 728 | query += " WHERE " + strings.Join(where, " AND ") 729 | } 730 | 731 | query += " ORDER BY " + in.Pagination.Order + " " + in.Pagination.Sort 732 | query += " LIMIT " + strconv.Itoa(int(in.Pagination.Limit)) 733 | query += " OFFSET " + strconv.Itoa(int(in.Pagination.Offset)) 734 | 735 | rows, err := u.db.QueryContext(ctx, query, paramQueries...) 736 | if err != nil { 737 | return out, logError(u.log, codes.Internal, err) 738 | } 739 | defer rows.Close() 740 | 741 | for rows.Next() { 742 | var obj drivers.Driver 743 | err = rows.Scan(&obj.Id, &obj.Name, &obj.Phone, &obj.LicenceNumber, &obj.CompanyId, &obj.CompanyName) 744 | if err != nil { 745 | return out, logError(u.log, codes.Internal, err) 746 | } 747 | 748 | out.Driver = append(out.Driver, &obj) 749 | } 750 | 751 | if rows.Err() != nil { 752 | return out, logError(u.log, codes.Internal, rows.Err()) 753 | } 754 | 755 | return out, nil 756 | } 757 | 758 | func (u *driverHandler) Create(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 759 | query := ` 760 | INSERT INTO drivers ( 761 | id, name, phone, licence_number, company_id, company_name, created, created_by, updated, updated_by) 762 | VALUES ($1, $2, $3 ,$4, $5, $6, $7, $8, $9, $10) 763 | ` 764 | in.Id = uuid.New().String() 765 | now := time.Now().Format("2006-01-02 15:04:05.000000") 766 | _, err := u.db.ExecContext(ctx, query, 767 | in.Id, in.Name, in.Phone, in.LicenceNumber, in.CompanyId, in.CompanyName, now, "jaka", now, "jaka") 768 | 769 | if err != nil { 770 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 771 | } 772 | 773 | return in, nil 774 | } 775 | 776 | func (u *driverHandler) Update(ctx context.Context, in *drivers.Driver) (*drivers.Driver, error) { 777 | query := ` 778 | UPDATE drivers 779 | SET name = $1, 780 | phone = $2, 781 | licence_number = $3, 782 | updated = $4, 783 | updated_by = $5 784 | WHERE id = $6 785 | ` 786 | now := time.Now().Format("2006-01-02 15:04:05.000000") 787 | _, err := u.db.ExecContext(ctx, query, 788 | in.Name, in.Phone, in.LicenceNumber, now, "jaka", in.Id) 789 | 790 | if err != nil { 791 | return &drivers.Driver{}, logError(u.log, codes.Internal, err) 792 | } 793 | 794 | return in, nil 795 | } 796 | 797 | func (u *driverHandler) Delete(ctx context.Context, in *generic.Id) (*generic.BoolMessage, error) { 798 | query := ` 799 | UPDATE drivers 800 | SET is_deleted = true 801 | WHERE id = $1 802 | ` 803 | _, err := u.db.ExecContext(ctx, query, in.Id) 804 | 805 | if err != nil { 806 | return &generic.BoolMessage{IsTrue: false}, logError(u.log, codes.Internal, err) 807 | } 808 | 809 | return &generic.BoolMessage{IsTrue: true}, nil 810 | } 811 | 812 | func logError(log *log.Logger, code codes.Code, err error) error { 813 | log.Print(err.Error()) 814 | return status.Error(code, err.Error()) 815 | } 816 | ``` 817 | 818 | --------------------------------------------------------------------------------