Go With Habib

Go With Habib Software Engineering Philosophy & backend with Go

আপনি যখন code লেখেন আর সেটা run করেন, তখন আসলে দুইটা আলাদা আলাদা জগতের মধ্য দিয়ে যেতে হয় — Compile Time আর Runtime। এই...
09/01/2026

আপনি যখন code লেখেন আর সেটা run করেন, তখন আসলে দুইটা আলাদা আলাদা জগতের মধ্য দিয়ে যেতে হয় — Compile Time আর Runtime। এই দুইটা concept programming-এর foundation, কিন্তু অনেকেই এদের মধ্যে পার্থক্য ঠিকমতো বোঝেন না।

আজকে আমরা deep dive করবো — compile time কী, runtime কী, কম্পিউটার কীভাবে code process করে, memory-তে কী ঘটে, আর computer scientists কেন এই পুরো system এভাবে design করলো। চলুন শুরু থেকে বুঝি।

The Fundamental Problem: মানুষ আর মেশিন আলাদা ভাষায় কথা বলে
সব কিছুর মূলে আছে একটা fundamental disconnect:

মানুষ চিন্তা করে: Variables, functions, loops, conditions — high-level abstractions-এ।

কম্পিউটার বোঝে: শুধু 0 আর 1 — binary instructions।

বিস্তারিত কমেন্টস এ।

ক্রেডিট: ইমরান হাসান

যারা Go programming শুরু করেছেন, তারা প্রায়ই একটা question করেন: "Go-এর standard library-তে তো database/sql আছে, তাহলে ...
01/01/2026

যারা Go programming শুরু করেছেন, তারা প্রায়ই একটা question করেন: "Go-এর standard library-তে তো database/sql আছে, তাহলে PostgreSQL-এর সাথে connect করতে github.com/lib/pq বা অন্য third-party library কেন লাগে? Standard library দিয়েই তো হওয়ার কথা!"

এটা একটা valid question। আর এই question-এর answer বুঝলে আপনি Go-এর design philosophy আর database architecture সম্পর্কে অনেক কিছু জানতে পারবেন। চলুন step by step বুঝি কেন এটা impossible, আর এভাবে design করার পেছনে কী কী চিন্তাভাবনা ছিল।

ক্রেডিট : ইমরান হাসান

আপনি যখন একটা function call করেন আর কিছু data pass করেন, তখন computer-এর ভেতরে আসলে কী ঘটে? এই simple দেখতে operation-এর...
29/12/2025

আপনি যখন একটা function call করেন আর কিছু data pass করেন, তখন computer-এর ভেতরে আসলে কী ঘটে? এই simple দেখতে operation-এর পেছনে আছে decades-এর research, design decisions, আর performance trade-offs। আজকে আমরা deep dive করবো — argument আর parameter কী, এরা কীভাবে কাজ করে, আর computer scientists কেন এভাবে design করলো।

বিস্তারিত কমেন্টস এ।
Credit: Imran Hasan

আপনি যদি Java, C++, বা Python থেকে Go-তে আসেন, তাহলে প্রথম দিকেই মনে হবে - "এখানে class নেই কেন? Inheritance কোথায়? ext...
22/10/2025

আপনি যদি Java, C++, বা Python থেকে Go-তে আসেন, তাহলে প্রথম দিকেই মনে হবে - "এখানে class নেই কেন? Inheritance কোথায়? extends keyword কই?"

এটা খুবই স্বাভাবিক প্রশ্ন। কারণ বেশিরভাগ modern programming languages যেখানে classes, inheritance, আর traditional object-oriented programming (OOP) paradigm follow করে, Go সেখানে একদম আলাদা পথ নিয়েছে।

কিন্তু এটা কি একটা design flaw? নাকি এর পেছনে কোনো গভীর reasoning আছে?

Link: https://www.linkedin.com/pulse/go-%E0%A6%95%E0%A6%A8-traditional-oop-%E0%A6%8F%E0%A6%B0-path-follow-%E0%A6%95%E0%A6%B0-%E0%A6%A8-imran-hasan-rqbtc/?trackingId=5grBX9mveEa0yQmWEScRVA%3D%3D

Credit: Imran Hasan

ভূমিকা: একটা প্রশ্ন তুমি যদি Java, C++, বা Python থেকে Go-তে আসো, তাহলে প্রথম দিকেই মনে হবে - "এখানে class নেই কেন? Inheritance কোথায়? extends keyword ক....

16/10/2025

Atomic operation একটা হার্ডওয়ার লেভেলের এক্টিভিটি।
আমাদের Go তে এই জিনিসটা ইউজ হয়। আমরা শুধু শুনছি যে এটমিক কিছু একটা হয় যখন মালটিথ্রেডিং নিয়ে কাজ করি।

ইউজার লেভেল কোড থেকে ইন্সট্রাকশন হয়ে যখন হার্ডওয়ার বা CPU তে এক্সিকিউট হয়, কনকারেন্ট resource request অপটিমালি হ্যান্ডেল করতে এটমিক এর ইউজেস টা তখন গো রানটাইমের গভীরে চুপি চুপি হতে থাকে।

আগেরবার সেমাফর ইমপ্লিমেন্টেশন নিয়ে বলছিলাম, এই সেমাফর লক আনলক করতে পারবে কিনা সেটার জন্য চেক পয়েন্ট ক্রিয়েট করে atomic operations.
Atomic operation ভেঙেচুরে আরেকটু ডিটেল দেয়ার চেষ্টা করছি আর্টিকেলে।
ধন্যবাদ।

Gratitude to Habibur Rahman

তোমাকে যদি বলা হয় "একটা router-কে সফটওয়্যার দিয়ে simulate করা যায়", তুমি কি বিশ্বাস করবে?হয়তো প্রথম প্রতিক্রিয়া হব...
16/10/2025

তোমাকে যদি বলা হয় "একটা router-কে সফটওয়্যার দিয়ে simulate করা যায়", তুমি কি বিশ্বাস করবে?

হয়তো প্রথম প্রতিক্রিয়া হবে, "এটা তো অসম্ভব! router তো একটা physical device, সেটা কীভাবে software-এ হতে পারে?"

কিন্তু এটাই computing-এর সবচেয়ে profound idea গুলোর একটা। আর এই article-এ আমরা গভীরভাবে বুঝব কেন এটা possible এবং কীভাবে এটা কাজ করে।

Credit: Imran Hasan

15/10/2025

ফেরিঘাট থেকে সার্ভার পর্যন্ত - Docker-এর গল্পটা একবার পড়ে দেখুন !

অনেকদিন পর আবার একটা লেখা লিখছি!
এইবারের বিষয়টা এমন কিছু, যেটা আধুনিক সফটওয়্যার ডেভেলপমেন্টের এক অনিবার্য অংশ Docker!

যদি Backend বা DevOps নিয়ে কাজ করেন, তাহলে Docker সম্পর্কে ধারণা থাকা এখন প্রায় অপরিহার্য।

Docker আসলে কী?
“Dock” মানে ফেরিঘাট - যেখানে জিনিসপত্র লোড ও আনলোড করা হয়।

আর যারা এই কাজগুলো করে, তাদের বলা হয় Docker।
ঠিক এই ধারণাটাই প্রযুক্তিতে ব্যবহার করা হয়েছে অসাধারণভাবে!

Docker হলো এমন একটি platform, যেখানে ডেভেলপাররা তাদের অ্যাপ্লিকেশনকে build, package, ship, এবং run করতে পারেন - একদম একই পরিবেশে, যেখানে সেটি তৈরি করা হয়েছিল।

এর মূল উপাদান হলো Container

এই container-এর ভেতরে থাকে
- আপনার code,
- প্রয়োজনীয় dependencies,
- এবং অ্যাপের configuration files।

অর্থাৎ, অ্যাপ চালানোর জন্য যা দরকার, সবকিছু একসাথে প্যাক করা থাকে একটি ছোট্ট, লাইটওয়েট প্যাকেজে।
Docker কীভাবে কাজ করে?

ধরুন, আপনি আপনার মেশিনে একটা অ্যাপ তৈরি করলেন - perfectly কাজ করছে।

কিন্তু যখন সার্ভারে ডেপ্লয় করলেন, তখন error আসছে।
এটাই সেই ক্লাসিক “works on my machine” সমস্যা!
Docker এই সমস্যার সমাধান করে দারুণভাবে

কারণ যখন আপনি অ্যাপটাকে Docker container-এর মধ্যে রাখেন, তখন সেটার সাথে environment-ও প্যাকেজ হয়ে যায়।

ফলে আপনি সেটাকে যেকোনো সার্ভার, ক্লাউড বা কম্পিউটারে চালাতে পারবেন, কোনো আলাদা সেটআপ ছাড়াই।
“If it runs on your machine, it will run anywhere.”

আমি কোথা থেকে শিখেছি :
আমি Docker সম্পর্কে এইরকম বিস্তারিত জানতে পারি Habib ভাই এর ক্লাসে।

ভাই এর পড়ানোর ধরণ এত সহজ, উদাহরণভিত্তিক আর স্পষ্ট,
যে জটিল টপিকও একদম crystal clear হয়ে যায়।



Credit: Salman Abdullah Fahim

তুমি হয়তো programming শিখেছ। Variable, loop, function, data structure - সব জানো। কিন্তু যখন একটা বড় system বানাতে বসো,...
15/10/2025

তুমি হয়তো programming শিখেছ। Variable, loop, function, data structure - সব জানো। কিন্তু যখন একটা বড় system বানাতে বসো, তখন মনে হয় কোথা থেকে শুরু করব? কীভাবে organize করব? Code লিখতে লিখতে হারিয়ে যাও একটা maze-এ।

এই feeling টা স্বাভাবিক। কারণ programming শেখা আর software design করা - এই দুটো আলাদা skill। Programming হলো tools জানা - hammer, screwdriver ইউজ করতে পারা। কিন্তু design হলো জানা কোথায় কোন tool ইউজ করতে হবে, কীভাবে একটা সুন্দর structure তৈরি করতে হবে।

আজকের এই article-এ আমরা গভীরভাবে জানব Low Level Design কী, কেন দরকার, এবং কীভাবে এটা তোমার software development skill-কে নিয়ে যাবে next level-এ।

credit: Imran Hasan

একটা HTTP Server দিয়ে পুরো ব্যাপারটা বুঝে নেওয়া যাকআমরা সবাই জানি Go একটা সিম্পল ল্যাঙ্গুয়েজ, কিন্তু interface নিয়ে ...
14/10/2025

একটা HTTP Server দিয়ে পুরো ব্যাপারটা বুঝে নেওয়া যাক

আমরা সবাই জানি Go একটা সিম্পল ল্যাঙ্গুয়েজ, কিন্তু interface নিয়ে অনেকেরই confusion থাকে। আজকে আমরা দেখব কীভাবে interface আসলে তোমার code-কে flexible এবং maintainable বানায়। একটা real-world HTTP server বানাতে বানাতে step by step বুঝব কেন interface ছাড়া code জট পাকিয়ে যায় আর interface দিয়ে কীভাবে সব ঝামেলা সলভ হয়ে যায়।

সমস্যা #১: Database-এর সাথে Tight Coupling
ধরো তুমি একটা user management API বানাচ্ছ। শুরুতে তুমি ভাবলে MongoDB ইউজ করবে। তাই সব জায়গায় MongoDB-এর code লিখে ফেললে।

প্রথম Version - সব জায়গায় MongoDB
package main

import (
"encoding/json"
"fmt"
"net/http"
)

// MongoDB struct - ধরো এটা আসল MongoDB connection
type MongoDB struct {
ConnectionString string
}

// MongoDB-তে user save করার method
func (db *MongoDB) SaveUser(username, email string) error {
fmt.Printf("MongoDB-তে save হচ্ছে: %s (%s)\n", username, email)
// এখানে actual MongoDB save logic থাকবে
return nil
}

// MongoDB থেকে user fetch করার method
func (db *MongoDB) GetUser(username string) (string, error) {
fmt.Printf("MongoDB থেকে fetch হচ্ছে: %s\n", username)
// এখানে actual MongoDB query logic থাকবে
return fmt.Sprintf("%[email protected]", username), nil
}

// HTTP handler যেটা user create করে
func CreateUserHandler(db *MongoDB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
email := r.URL.Query().Get("email")

// সরাসরি MongoDB-এর method call করছি
err := db.SaveUser(username, email)
if err != nil {
http.Error(w, "Error saving user", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"message": "User created successfully",
})
}
}

// HTTP handler যেটা user fetch করে
func GetUserHandler(db *MongoDB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")

// আবারও সরাসরি MongoDB-এর method call
email, err := db.GetUser(username)
if err != nil {
http.Error(w, "Error fetching user", http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(map[string]string{
"username": username,
"email": email,
})
}
}

func main() {
// MongoDB connection setup
db := &MongoDB{
ConnectionString: "mongodb://localhost:27017",
}

// Routes setup
http.HandleFunc("/user/create", CreateUserHandler(db))
http.HandleFunc("/user/get", GetUserHandler(db))

fmt.Println("Server চালু হয়েছে port 8080-তে")
http.ListenAndServe(":8080", nil)
}

এই Code-এর সমস্যাগুলো কী?
এবার চিন্তা করো তোমার boss বললো, "আমরা PostgreSQL-এ switch করব কারণ MongoDB expensive হয়ে যাচ্ছে।" তখন তোমাকে কী করতে হবে?

প্রথমত, তোমাকে পুরো codebase-এ গিয়ে প্রতিটা জায়গায় *MongoDB খুঁজে বের করে সেটা *PostgreSQL বানাতে হবে। দেখো কত জায়গায় problem হবে:

সমস্যা ১.১: Handler Functions পুরোপুরি MongoDB-এর উপর নির্ভরশীল

CreateUserHandler আর GetUserHandler দুটোই *MongoDB parameter হিসেবে নিচ্ছে। এর মানে হলো এই handlers শুধুমাত্র MongoDB-এর সাথেই কাজ করতে পারে। তুমি যদি PostgreSQL বা কোনো in-memory database ইউজ করতে চাও, তাহলে এই পুরো handler functions আবার নতুন করে লিখতে হবে।

সমস্যা ১.২: Testing করা প্রায় impossible

ধরো তুমি এই handlers-এর unit test লিখতে চাচ্ছ। কিন্তু test লেখার জন্য তোমাকে একটা real MongoDB instance run করতে হবে। এটা অনেক slow এবং test setup খুবই complicated হয়ে যায়। তুমি চাইলে fake বা mock database দিয়ে test করতে পারবে না, কারণ handler শুধুমাত্র *MongoDB type-ই accept করে।

সমস্যা ১.৩: Code reuse করা যায় না

মনে করো তোমার আরেকটা microservice আছে যেটা MySQL ইউজ করে। তুমি চাইলেও এই handlers সেখানে ইউজ করতে পারবে না। কারণ? এগুলো hardcoded MongoDB-এর জন্য বানানো।

সমস্যা ১.৪: পুরো codebase change করতে হয়

Database switch করতে গেলে শুধু database layer-এই change করলেই হয় না। তোমাকে সব handler, সব function যেখানে *MongoDB আছে সব জায়গায় গিয়ে change করতে হবে। এটা error-prone এবং সময়সাপেক্ষ।

এই সমস্যাটাকে বলা হয় tight coupling। মানে তোমার business logic (HTTP handlers) আর তোমার infrastructure (database) একসাথে জড়িয়ে আছে। এদের আলাদা করা যাচ্ছে না।

সমাধান #১: Interface দিয়ে Abstraction তৈরি করা
এবার দেখো কীভাবে interface দিয়ে এই problem solve করা যায়। মূল idea হলো, "আমার handler-এর জানা দরকার না database কোনটা। শুধু জানা দরকার সে কী কী operation করতে পারে।"

Interface দিয়ে Decoupled Version
package main

import (
"encoding/json"
"fmt"
"net/http"
)

// ১. প্রথমে একটা interface define করি যেটা বলে দেয়
// আমাদের কী কী capability দরকার
type UserRepository interface {
SaveUser(username, email string) error
GetUser(username string) (string, error)
}

// ২. MongoDB implementation - interface satisfy করছে
type MongoDB struct {
ConnectionString string
}

func (db *MongoDB) SaveUser(username, email string) error {
fmt.Printf("MongoDB-তে save হচ্ছে: %s (%s)\n", username, email)
return nil
}

func (db *MongoDB) GetUser(username string) (string, error) {
fmt.Printf("MongoDB থেকে fetch হচ্ছে: %s\n", username)
return fmt.Sprintf("%[email protected]", username), nil
}

// ৩. PostgreSQL implementation - একই interface satisfy করছে
type PostgreSQL struct {
ConnectionString string
}

func (db *PostgreSQL) SaveUser(username, email string) error {
fmt.Printf("PostgreSQL-এ save হচ্ছে: %s (%s)\n", username, email)
return nil
}

func (db *PostgreSQL) GetUser(username string) (string, error) {
fmt.Printf("PostgreSQL থেকে fetch হচ্ছে: %s\n", username)
return fmt.Sprintf("%[email protected]", username), nil
}

// ৪. এবার handler concrete type-এর বদলে interface নিচ্ছে
func CreateUserHandler(repo UserRepository) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
email := r.URL.Query().Get("email")

// এখন repo MongoDB-ও হতে পারে, PostgreSQL-ও হতে পারে
// handler-এর কিছু যায় আসে না
err := repo.SaveUser(username, email)
if err != nil {
http.Error(w, "Error saving user", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"message": "User created successfully",
})
}
}

func GetUserHandler(repo UserRepository) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")

email, err := repo.GetUser(username)
if err != nil {
http.Error(w, "Error fetching user", http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(map[string]string{
"username": username,
"email": email,
})
}
}

func main() {
// এখন তুমি যেকোনো implementation ইউজ করতে পারো

// MongoDB ইউজ করতে চাইলে:
// var repo UserRepository = &MongoDB{ConnectionString: "mongodb://localhost:27017"}

// PostgreSQL ইউজ করতে চাইলে:
var repo UserRepository = &PostgreSQL{ConnectionString: "postgres://localhost:5432"}

// Handler setup - একই handlers কাজ করবে যেকোনো database-এর সাথে
http.HandleFunc("/user/create", CreateUserHandler(repo))
http.HandleFunc("/user/get", GetUserHandler(repo))

fmt.Println("Server চালু হয়েছে port 8080-তে")
http.ListenAndServe(":8080", nil)
}

কী পরিবর্তন হলো এবং কেন এটা Better?
চলো step by step দেখি কী কী advantage পেলাম:

সুবিধা ১.১: Handlers এখন Database-Independent

লক্ষ করো, CreateUserHandler এবং GetUserHandler এখন UserRepository interface নিচ্ছে, কোনো concrete type নয়। এর মানে হলো এই handlers যেকোনো database-এর সাথে কাজ করতে পারবে যেটা UserRepository interface implement করে। তোমাকে শুধু SaveUser আর GetUser methods implement করতে হবে, সব কাজ হয়ে যাবে।

সুবিধা ১.২: Database Switch করা এখন এক লাইনের কাজ

main() function-এ দেখো, MongoDB থেকে PostgreSQL-এ switch করতে শুধু একটা লাইন comment/uncomment করলেই হচ্ছে। পুরো application-এর আর কোনো code touch করতে হচ্ছে না। এটাই হলো decoupling-এর power।

সুবিধা ১.৩: Testing এখন অনেক সহজ

এখন তুমি সহজেই mock repository বানিয়ে test করতে পারবে, যেটা পরে আমরা দেখব। Real database ছাড়াই handlers test করা যাবে।

সুবিধা ১.৪: Code Reusability বেড়ে গেছে

এই handlers এখন যেকোনো project-এ ইউজ করা যাবে। শুধু সেই project-এর database-এর জন্য UserRepository interface implement করলেই হবে।

সমস্যা #২: Testing করা যাচ্ছে না
এবার আরেকটা বড় সমস্যা নিয়ে কথা বলি। তুমি যখন প্রথম coupled version-এ unit test লিখতে যাবে, তখন দেখবে অনেক ঝামেলা। কারণ test চালাতে হলে তোমাকে একটা real MongoDB instance run করতে হবে।

Testing-এর সমস্যাগুলো Coupled Code-এ
চলো দেখি coupled code-এ test লিখতে গেলে কী কী problem হয়:

সমস্যা ২.১: Test Setup অনেক জটিল

প্রতিবার test run করার আগে তোমাকে MongoDB start করতে হবে, database create করতে হবে, connection setup করতে হবে। এটা শুধু সময়সাপেক্ষই না, test environment setup করাও complicated।

সমস্যা ২.২: Tests অনেক Slow

Real database-এর সাথে interaction করতে হচ্ছে, তাই প্রতিটা test অনেক সময় নিচ্ছে। একটা simple handler test করতে হয়তো ১-২ সেকেন্ড লাগছে। যখন তোমার ১০০টা test হবে, তখন পুরো test suite চালাতে মিনিটের পর মিনিট লাগবে।

সমস্যা ২.৩: Test Isolation নেই

একটা test যদি database-এ data লিখে রাখে, সেটা অন্য test-কে affect করতে পারে। তোমাকে প্রতিটা test-এর আগে database clean করতে হবে, যা আরও complexity add করে।

সমস্যা ২.৪: CI/CD Pipeline-এ Problem

GitHub Actions বা GitLab CI-তে test run করতে গেলে সেখানেও MongoDB setup করতে হবে। এটা pipeline-কে slow এবং unreliable বানায়।

Coupled Code-এ Test লেখার চেষ্টা
চলো দেখি প্রথম version-এ test লিখতে গেলে কেমন দেখাতো:

package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestCreateUserHandler_Coupled(t *testing.T) {
// সমস্যা: আমাকে real MongoDB connection দিতেই হবে
db := &MongoDB{
ConnectionString: "mongodb://localhost:27017",
}

// এই test run করার আগে MongoDB চালু থাকতে হবে!
// তা না হলে test fail করবে

handler := CreateUserHandler(db)

req := httptest.NewRequest("GET", "/user/create?username=test&email=[email protected]", nil)
w := httptest.NewRecorder()

handler(w, req)

// Test করছি response
if w.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", w.Code)
}

// কিন্তু এই test actually MongoDB-তে data write করে ফেলছে!
// এটা test না, এটা integration test হয়ে গেছে
}

এই test-এর problems:

প্রথম সমস্যা: MongoDB না চললে test fail করবে। মানে তোমার development machine-এ MongoDB install না থাকলে বা service down থাকলে test চলবে না।

দ্বিতীয় সমস্যা: এটা actually database-এ write করছে। Pure unit test হওয়া উচিত ছিল, কিন্তু এটা database-এর উপর depend করছে।

তৃতীয় সমস্যা: Test slow। একটা simple handler test করতে database connection, write operation সব করতে হচ্ছে।

সমাধান #২: Mock Repository দিয়ে Fast Testing
এবার দেখো কীভাবে interface ইউজ করে testing problem solve হয়। Interface থাকার কারণে তুমি খুব সহজেই একটা fake/mock implementation বানাতে পারো যেটা কোনো real database ছাড়াই কাজ করবে।

Mock Repository Implementation
package main

import (
"fmt"
)

// Mock implementation যেটা memory-তে data রাখে
// এটা testing-এর জন্য perfect
type MockRepository struct {
// Memory-তে user data রাখার জন্য map
users map[string]string

// Test করার জন্য track করি কতবার methods call হলো
SaveUserCalled int
GetUserCalled int
}

// MockRepository-র constructor
func NewMockRepository() *MockRepository {
return &MockRepository{
users: make(map[string]string),
}
}

// UserRepository interface implement করছি
func (m *MockRepository) SaveUser(username, email string) error {
m.SaveUserCalled++ // Counter বাড়াচ্ছি
m.users[username] = email // Memory-তে save করছি
fmt.Printf("Mock: User saved in memory - %s (%s)\n", username, email)
return nil
}

func (m *MockRepository) GetUser(username string) (string, error) {
m.GetUserCalled++ // Counter বাড়াচ্ছি

email, exists := m.users[username]
if !exists {
return "", fmt.Errorf("user not found")
}

fmt.Printf("Mock: User fetched from memory - %s\n", username)
return email, nil
}

এবার Test লিখি Mock দিয়ে
package main

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)

func TestCreateUserHandler_WithMock(t *testing.T) {
// Real database-এর বদলে mock ইউজ করছি
// কোনো external dependency নেই!
mockRepo := NewMockRepository()

// Handler create করছি mock দিয়ে
handler := CreateUserHandler(mockRepo)

// HTTP request simulate করছি
req := httptest.NewRequest("GET", "/user/create?username=ahmed&email=[email protected]", nil)
w := httptest.NewRecorder()

// Handler call করছি
handler(w, req)

// Response check করছি
if w.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", w.Code)
}

// Verify করছি যে SaveUser actually call হয়েছে কিনা
if mockRepo.SaveUserCalled != 1 {
t.Errorf("Expected SaveUser to be called once, called %d times", mockRepo.SaveUserCalled)
}

// Verify করছি data সঠিকভাবে save হয়েছে কিনা
email, err := mockRepo.GetUser("ahmed")
if err != nil {
t.Errorf("User should exist in mock repository")
}
if email != "[email protected]" {
t.Errorf("Expected email [email protected], got %s", email)
}

// Response body check করছি
var response map[string]string
json.NewDecoder(w.Body).Decode(&response)

if response["message"] != "User created successfully" {
t.Errorf("Unexpected response message: %s", response["message"])
}
}

func TestGetUserHandler_WithMock(t *testing.T) {
mockRepo := NewMockRepository()

// Test data setup - প্রথমে একটা user add করছি
mockRepo.SaveUser("test_user", "[email protected]")

handler := GetUserHandler(mockRepo)

req := httptest.NewRequest("GET", "/user/get?username=test_user", nil)
w := httptest.NewRecorder()

handler(w, req)

// Status check
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}

// GetUser call হয়েছে কিনা check
if mockRepo.GetUserCalled != 1 {
t.Errorf("Expected GetUser to be called once, called %d times", mockRepo.GetUserCalled)
}

// Response data verify
var response map[string]string
json.NewDecoder(w.Body).Decode(&response)

if response["username"] != "test_user" {
t.Errorf("Expected username test_user, got %s", response["username"])
}
if response["email"] != "[email protected]" {
t.Errorf("Expected email [email protected], got %s", response["email"])
}
}

func TestGetUserHandler_UserNotFound(t *testing.T) {
mockRepo := NewMockRepository()

// কোনো user add করছি না, তাই "not found" error আসা উচিত

handler := GetUserHandler(mockRepo)

req := httptest.NewRequest("GET", "/user/get?username=nonexistent", nil)
w := httptest.NewRecorder()

handler(w, req)

// এবার error response expect করছি
if w.Code != http.StatusInternalServerError {
t.Errorf("Expected status 500, got %d", w.Code)
}
}

Mock Testing-এর সুবিধাগুলো
সুবিধা ২.১: কোনো External Dependency নেই

দেখো, test চালাতে আমার MongoDB, PostgreSQL কিছুই লাগছে না। MockRepository সব কিছু memory-তে করছে। এর মানে হলো যেকোনো machine-এ, যেকোনো সময় এই tests run করা যাবে।

সুবিধা ২.২: Tests অনেক Fast

Real database access নেই, তাই প্রতিটা test milliseconds-এ শেষ হয়ে যাচ্ছে। ১০০টা test থাকলেও ১ সেকেন্ডের মধ্যে শেষ হয়ে যাবে।

সুবিধা ২.৩: Test Behavior Control করা যায়

MockRepository-তে তুমি যেকোনো scenario simulate করতে পারো। ধরো তুমি test করতে চাও database connection fail হলে কী হয়। Mock-এ সহজেই error return করতে পারো:

func (m *MockRepository) SaveUser(username, email string) error {
// Simulate database failure
if username == "fail_test" {
return fmt.Errorf("database connection failed")
}
m.users[username] = email
return nil
}

এভাবে different scenarios test করা যায় real database ছাড়াই।

সুবিধা ২.৪: CI/CD-তে সহজে Run হয়

GitHub Actions, GitLab CI বা যেকোনো CI/CD pipeline-এ এই tests কোনো setup ছাড়াই run হবে। কারণ কোনো external service লাগছে না।

সমস্যা #৩: নতুন Feature Add করা কঠিন
এখন ধরো তোমাকে একটা নতুন feature add করতে বলা হলো: user-দের activity log করতে হবে। প্রতিবার কোনো user create বা fetch হলে সেটা log file-এ লিখতে হবে।

Coupled Code-এ এই Feature Add করতে গেলে
যদি তোমার code coupled থাকে, তাহলে তোমাকে প্রতিটা handler modify করতে হবে:

// এভাবে করতে হবে coupled code-এ
func CreateUserHandler(db *MongoDB, logger *FileLogger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
email := r.URL.Query().Get("email")

// Database operation
err := db.SaveUser(username, email)
if err != nil {
logger.LogError("Failed to save user", err)
http.Error(w, "Error saving user", http.StatusInternalServerError)
return
}

// Logging করতে হচ্ছে manually
logger.LogInfo(fmt.Sprintf("User created: %s", username))

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"message": "User created successfully",
})
}
}

এই approach-এর সমস্যা:

সমস্যা ৩.১: প্রতিটা Handler Modify করতে হচ্ছে

তোমার যত handlers আছে সবগুলোতে logging logic add করতে হবে। এটা error-prone এবং repetitive।

সমস্যা ৩.২: Multiple Dependencies

এখন handler দুটো concrete type-এর উপর depend করছে - *MongoDB আর *FileLogger। আরও dependency add হলে code আরও messy হবে।

সমস্যা ৩.৩: Single Responsibility Principle ভাঙছে

Handler-এর responsibility হওয়া উচিত শুধু HTTP request handle করা। কিন্তু এখন সে logging-ও করছে, database-ও handle করছে।

সমাধান #৩: Decorator Pattern দিয়ে Feature Add করা
Interface ইউজ করলে তুমি decorator pattern apply করতে পারো। এর মানে হলো তুমি একটা repository-কে অন্য repository দিয়ে wrap করতে পারো যেটা extra functionality add করবে, কিন্তু original behavior change করবে না।

Logging Repository Decorator
package main

import (
"fmt"
"time"
)

// LoggingRepository যেটা অন্য repository-কে wrap করে
// এবং সব operations log করে
type LoggingRepository struct {
// Wrapped repository - এটা যেকোনো UserRepository হতে পারে
wrapped UserRepository
logFile string
}

// Constructor যেটা যেকোনো repository wrap করতে পারে
func NewLoggingRepository(repo UserRepository) *LoggingRepository {
return &LoggingRepository{
wrapped: repo,
logFile: "user_activity.log",
}
}

// UserRepository interface implement করছি
// কিন্তু actual কাজ wrapped repository-তে delegate করছি
func (lr *LoggingRepository) SaveUser(username, email string) error {
// Operation করার আগে log
start := time.Now()
lr.log(fmt.Sprintf("SaveUser called for username: %s", username))

// Actual operation wrapped repository-কে দিয়ে করাচ্ছি
err := lr.wrapped.SaveUser(username, email)

// Operation শেষে log
duration := time.Since(start)
if err != nil {
lr.log(fmt.Sprintf("SaveUser failed for %s: %v (took %v)", username, err, duration))
} else {
lr.log(fmt.Sprintf("SaveUser succeeded for %s (took %v)", username, duration))
}

return err
}

func (lr *LoggingRepository) GetUser(username string) (string, error) {
start := time.Now()
lr.log(fmt.Sprintf("GetUser called for username: %s", username))

// Actual operation
email, err := lr.wrapped.GetUser(username)

duration := time.Since(start)
if err != nil {
lr.log(fmt.Sprintf("GetUser failed for %s: %v (took %v)", username, err, duration))
} else {
lr.log(fmt.Sprintf("GetUser succeeded for %s, email: %s (took %v)", username, email, duration))
}

return email, err
}

// Helper method logging-এর জন্য
func (lr *LoggingRepository) log(message string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
fmt.Print(logEntry)
// এখানে actual file-এ write করতে পারো
}

এবার দেখো কত সহজে ইউজ করা যায়
func main() {
// ১. প্রথমে base repository create করি
baseRepo := &PostgreSQL{
ConnectionString: "postgres://localhost:5432",
}

// ২. তারপর সেটাকে logging দিয়ে wrap করি
// এখন সব operations automatically log হবে!
loggedRepo := NewLoggingRepository(baseRepo)

// ৩. Handlers-এ pass করি
// Handlers-এর কোনো code change করতে হয়নি!
http.HandleFunc("/user/create", CreateUserHandler(loggedRepo))
http.HandleFunc("/user/get", GetUserHandler(loggedRepo))

fmt.Println("Server চালু হয়েছে port 8080-তে")
http.ListenAndServe(":8080", nil)
}

Decorator Pattern-এর Power
সুবিধা ৩.১: Zero Code Change in Handlers

লক্ষ করো, handlers-এর একটা লাইনও change করতে হয়নি। শুধু main() function-এ repository wrap করে দিয়েছি, সব logging automatically হচ্ছে।

সুবিধা ৩.২: Composable

তুমি চাইলে multiple decorators stack করতে পারো:

func main() {
baseRepo := &PostgreSQL{ConnectionString: "..."}

// প্রথমে logging wrap
loggedRepo := NewLoggingRepository(baseRepo)

// তারপর caching wrap
cachedRepo := NewCachingRepository(loggedRepo)

// তারপর retry logic wrap
finalRepo := NewRetryRepository(cachedRepo, 3)

// সব features একসাথে পেয়ে গেলে!
http.HandleFunc("/user/create", CreateUserHandler(finalRepo))
}

সুবিধা ৩.৩: Easy to Test Individually

প্রতিটা decorator আলাদাভাবে test করা যায়:

func TestLoggingRepository(t *testing.T) {
// Mock base repository
mockRepo := NewMockRepository()

// Wrap with logging
loggedRepo := NewLoggingRepository(mockRepo)

// Test logging behavior
loggedRepo.SaveUser("test", "[email protected]")

// Verify mock was called
if mockRepo.SaveUserCalled != 1 {
t.Error("Base repository should be called")
}
}

সমস্যা #৪: Production আর Development Environment আলাদা
আরেকটা common problem হলো, development-এ তুমি হয়তো local SQLite database ইউজ করছ, কিন্তু production-এ AWS RDS PostgreSQL ইউজ করতে হবে। এছাড়া staging environment-এ আবার আলাদা setup থাকতে পারে।

Environment-Specific Configuration
Coupled code-এ এই scenario handle করা অনেক কঠিন। তোমাকে lots of if-else লিখতে হবে:

// Coupled approach
func setupDatabase(env string) interface{} {
if env == "development" {
return &SQLite{Path: "./dev.db"}
} else if env == "staging" {
return &PostgreSQL{ConnectionString: "staging-url"}
} else if env == "production" {
return &PostgreSQL{ConnectionString: "production-url"}
}
return nil
}

// Problem: return type interface{}, type safety নেই
// আরও problem: এই function ইউজ করতে গেলে type assertion করতে হবে

সমাধান #৪: Interface দিয়ে Clean Environment Handling
Interface থাকলে তুমি খুব সহজেই environment-specific implementations ইউজ করতে পারো:

package main

import (
"fmt"
"os"
)

// Factory function যেটা environment অনুযায়ী সঠিক repository return করে
func NewRepository(env string) UserRepository {
switch env {
case "development":
// Development-এ in-memory mock ইউজ করি fast iteration-এর জন্য
fmt.Println("Using mock repository for development")
return NewMockRepository()

case "staging":
// Staging-এ PostgreSQL test database
fmt.Println("Using PostgreSQL for staging")
return &PostgreSQL{
ConnectionString: os.Getenv("STAGING_DB_URL"),
}

case "production":
// Production-এ actual PostgreSQL with connection pooling
fmt.Println("Using PostgreSQL for production")
prodRepo := &PostgreSQL{
ConnectionString: os.Getenv("PRODUCTION_DB_URL"),
}

// Production-এ logging আর caching add করি
loggedRepo := NewLoggingRepository(prodRepo)
cachedRepo := NewCachingRepository(loggedRepo)

return cachedRepo

default:
panic(fmt.Sprintf("Unknown environment: %s", env))
}
}

func main() {
// Environment variable থেকে পড়ি
env := os.Getenv("APP_ENV")
if env == "" {
env = "development" // Default
}

// সঠিক repository পাই
repo := NewRepository(env)

// বাকি সব একই থাকে
http.HandleFunc("/user/create", CreateUserHandler(repo))
http.HandleFunc("/user/get", GetUserHandler(repo))

fmt.Printf("Server চালু হয়েছে %s environment-এ\n", env)
http.ListenAndServe(":8080", nil)
}

এই Approach-এর সুবিধা
সুবিধা ৪.১: Type Safety

Factory function UserRepository interface return করছে, তাই compile time-এই type check হবে। কোনো runtime type assertion লাগছে না।

সুবিধা ৪.২: Clear Configuration

Environment-specific logic একটা জায়গায় centralized। তোমার পুরো codebase-এ if-else scattered না।

সুবিধা ৪.৩: Easy to Add New Environments

নতুন environment add করতে চাইলে শুধু factory function-এ একটা case add করলেই হবে।

সুবিধা ৪.৪: Consistent Interface

Application code কখনও জানে না কোন environment-এ run হচ্ছে। সব জায়গায় same UserRepository interface ইউজ হচ্ছে।

সমস্যা #৫: Third-Party Library Integration করা কঠিন
এখন ধরো তুমি একটা third-party caching library ইউজ করতে চাও, যেমন Redis। কিন্তু সেই library-র API তোমার current code structure-এর সাথে match করছে না।

Third-Party API যেটা আমাদের Pattern-এ Fit করছে না
// ধরো Redis library-র API এরকম:
type RedisClient struct {
host string
port int
}

func (r *RedisClient) Connect() error { /* ... */ }
func (r *RedisClient) SetValue(key, value string) error { /* ... */ }
func (r *RedisClient) GetValue(key string) (string, bool) { /* ... */ }

// সমস্যা: এই API আমাদের UserRepository interface-এর সাথে match করে না
// SetValue/GetValue আছে, কিন্তু SaveUser/GetUser নেই

Coupled code-এ তোমাকে পুরো application modify করতে হবে এই library ইউজ করতে। কিন্তু interface থাকলে?

সমাধান #৫: Adapter Pattern দিয়ে Third-Party Integration
Interface-এর আরেকটা powerful use case হলো adapter pattern। তুমি যেকোনো third-party library-কে তোমার interface-এ adapt করতে পারো।

Redis Adapter বানানো
package main

import (
"encoding/json"
"fmt"
)

// Redis adapter যেটা RedisClient-কে UserRepository-তে convert করে
type RedisUserRepository struct {
client *RedisClient
}

func NewRedisUserRepository(host string, port int) (*RedisUserRepository, error) {
client := &RedisClient{
host: host,
port: port,
}

err := client.Connect()
if err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
}

return &RedisUserRepository{
client: client,
}, nil
}

// এবার UserRepository interface implement করি
// Redis API-কে আমাদের interface-এ adapt করছি
func (r *RedisUserRepository) SaveUser(username, email string) error {
// Redis-এ JSON হিসেবে save করছি
userData := map[string]string{
"username": username,
"email": email,
}

jsonData, err := json.Marshal(userData)
if err != nil {
return err
}

// Redis library-র SetValue method ইউজ করছি
key := fmt.Sprintf("user:%s", username)
return r.client.SetValue(key, string(jsonData))
}

func (r *RedisUserRepository) GetUser(username string) (string, error) {
key := fmt.Sprintf("user:%s", username)

// Redis library-র GetValue method ইউজ করছি
jsonData, exists := r.client.GetValue(key)
if !exists {
return "", fmt.Errorf("user not found")
}

// JSON parse করছি
var userData map[string]string
err := json.Unmarshal([]byte(jsonData), &userData)
if err != nil {
return "", err
}

return userData["email"], nil
}

এবার যেকোনো জায়গায় ইউজ করা যাবে
func main() {
// Redis repository create করি
redisRepo, err := NewRedisUserRepository("localhost", 6379)
if err != nil {
panic(err)
}

// Application code-এ কোনো change লাগছে না!
// কারণ এটাও UserRepository interface implement করে
http.HandleFunc("/user/create", CreateUserHandler(redisRepo))
http.HandleFunc("/user/get", GetUserHandler(redisRepo))

fmt.Println("Server চালু হয়েছে Redis-এর সাথে")
http.ListenAndServe(":8080", nil)
}

Adapter Pattern-এর Power
সুবিধা ৫.১: Vendor Lock-in থেকে মুক্তি

তুমি যদি ভবিষ্যতে Redis থেকে Memcached-এ switch করতে চাও, শুধু adapter change করলেই হবে। Application logic touch করতে হবে না।

সুবিধা ৫.২: Multiple Implementations একসাথে ইউজ করা

func main() {
// Primary database PostgreSQL
primaryRepo := &PostgreSQL{ConnectionString: "..."}

// Cache layer Redis
cacheRepo, _ := NewRedisUserRepository("localhost", 6379)

// দুটো একসাথে ইউজ করি - caching strategy implement করছি
finalRepo := NewCachingRepository(primaryRepo, cacheRepo)

http.HandleFunc("/user/create", CreateUserHandler(finalRepo))
}

সুবিধা ৫.৩: Library Upgrade সহজ

Redis library নতুন version-এ API change করলে শুধু adapter update করলেই হবে। Whole application rewrite করতে হবে না।

Interface-এর Actual Power কোথায়?
এতক্ষণ আমরা অনেকগুলো problem আর solution দেখলাম। এবার চলো summarize করি interface actually কী কী power দেয়:

Power #1: Dependency Inversion
High-level code (handlers) আর low-level code (database) এর মধ্যে abstraction layer তৈরি হয়। High-level code কখনও low-level details জানে না। এটাকে বলে Dependency Inversion Principle।

Traditional dependency:

Handler → PostgreSQL
(high-level depends on low-level)

With interface:

Handler → UserRepository Interface ← PostgreSQL
(both depend on abstraction)

Power #2: Polymorphism
একই interface multiple implementations থাকতে পারে, আর runtime-এ decide করা যায় কোনটা ইউজ হবে। এটা traditional object-oriented polymorphism-এর একটা form।

// একই handler, different implementations
var repo UserRepository

repo = &PostgreSQL{...} // SQL database
repo = &MongoDB{...} // NoSQL database
repo = NewMockRepository() // Testing
repo = &RedisUserRepository{...} // Cache

Power #3: Open/Closed Principle
তোমার code "open for extension, closed for modification"। মানে নতুন functionality add করতে পারো existing code না ছুঁয়ে।

// নতুন feature? নতুন implementation লিখো
type AuditedRepository struct {
wrapped UserRepository
}

// Existing handlers modify করতে হবে না!

Power #4: Testability
Testing অনেক সহজ হয়ে যায় কারণ তুমি real dependencies-র বদলে mocks/stubs inject করতে পারো।

Power #5: Flexibility
Future requirements change হলে easily adapt করা যায়। Database switch করতে চাও? New caching layer add করতে চাও? Monitoring add করতে চাও? সব easily possible।

শেষ কথা: Interface কখন ইউজ করবে?
Interface খুবই powerful, কিন্তু সব জায়গায় ইউজ করার দরকার নেই। এখানে কিছু guideline:

Interface ইউজ করো যখন:
১. Multiple Implementations সম্ভব যদি একটা functionality-র multiple way of implementation থাকতে পারে, তাহলে interface ইউজ করো। যেমন database, file storage, message queue ইত্যাদি।

২. Testing Dependency আছে যদি কোনো external system (database, API, file system) এর সাথে interaction করতে হয়, interface ইউজ করলে testing সহজ হয়।

৩. Behavior Abstraction দরকার যখন তুমি "কী করতে হবে" define করতে চাও, "কীভাবে করতে হবে" নয়। Interface হলো behavioral contract।

৪. Extensibility চাও Future-এ নতুন functionality add করার সম্ভাবনা থাকলে interface ইউজ করো।

Interface ইউজ করো না যখন:
১. শুধু একটা Implementation যদি নিশ্চিত যে কখনও alternate implementation লাগবে না, premature abstraction করো না।

২. Simple Helper Functions calculateTax(), formatDate() এরকম simple utility functions-এর জন্য interface overkill।

৩. Internal/Private Code Package-এর internal implementation details-এ interface unnecessary complexity add করতে পারে।

পুরো Example একসাথে
চলো শেষবারের মতো পুরো example একসাথে দেখি, সব concepts apply করে:

package main

import (
"encoding/json"
"fmt"
"net/http"
"os"
)

// ========== Interface Definition ==========
type UserRepository interface {
SaveUser(username, email string) error
GetUser(username string) (string, error)
}

// ========== Implementations ==========

// PostgreSQL implementation
type PostgreSQL struct {
ConnectionString string
}

func (db *PostgreSQL) SaveUser(username, email string) error {
fmt.Printf("PostgreSQL: Saving %s (%s)\n", username, email)
return nil
}

func (db *PostgreSQL) GetUser(username string) (string, error) {
fmt.Printf("PostgreSQL: Fetching %s\n", username)
return fmt.Sprintf("%[email protected]", username), nil
}

// Mock implementation for testing
type MockRepository struct {
users map[string]string
}

func NewMockRepository() *MockRepository {
return &MockRepository{users: make(map[string]string)}
}

func (m *MockRepository) SaveUser(username, email string) error {
m.users[username] = email
return nil
}

func (m *MockRepository) GetUser(username string) (string, error) {
if email, ok := m.users[username]; ok {
return email, nil
}
return "", fmt.Errorf("user not found")
}

// ========== Decorators ==========

// Logging decorator
type LoggingRepository struct {
wrapped UserRepository
}

func NewLoggingRepository(repo UserRepository) *LoggingRepository {
return &LoggingRepository{wrapped: repo}
}

func (lr *LoggingRepository) SaveUser(username, email string) error {
fmt.Printf("LOG: SaveUser called for %s\n", username)
err := lr.wrapped.SaveUser(username, email)
if err != nil {
fmt.Printf("LOG: SaveUser failed: %v\n", err)
}
return err
}

func (lr *LoggingRepository) GetUser(username string) (string, error) {
fmt.Printf("LOG: GetUser called for %s\n", username)
return lr.wrapped.GetUser(username)
}

// ========== HTTP Handlers ==========

func CreateUserHandler(repo UserRepository) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
email := r.URL.Query().Get("email")

err := repo.SaveUser(username, email)
if err != nil {
http.Error(w, "Error saving user", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"message": "User created successfully",
})
}
}

func GetUserHandler(repo UserRepository) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")

email, err := repo.GetUser(username)
if err != nil {
http.Error(w, "Error fetching user", http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(map[string]string{
"username": username,
"email": email,
})
}
}

// ========== Factory ==========

func NewRepository(env string) UserRepository {
var baseRepo UserRepository

switch env {
case "development":
baseRepo = NewMockRepository()
case "production":
baseRepo = &PostgreSQL{
ConnectionString: os.Getenv("DB_URL"),
}
default:
baseRepo = NewMockRepository()
}

// Production-এ logging add করি
if env == "production" {
return NewLoggingRepository(baseRepo)
}

return baseRepo
}

// ========== Main ==========

func main() {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}

repo := NewRepository(env)

http.HandleFunc("/user/create", CreateUserHandler(repo))
http.HandleFunc("/user/get", GetUserHandler(repo))

fmt.Printf("Server running in %s mode on :8080\n", env)
http.ListenAndServe(":8080", nil)
}

এই final example-এ তুমি দেখতে পাচ্ছো কীভাবে:

Interface দিয়ে database abstraction করা হয়েছে

Multiple implementations (PostgreSQL, Mock) আছে

Decorator pattern দিয়ে logging add করা হয়েছে

Environment-specific configuration করা হয়েছে

Handlers পুরোপুরি decoupled এবং testable

Interface-এর মূল শক্তি হলো এটা তোমার code-কে flexible, maintainable এবং testable বানায়। এটা শুধু Go-তেই নয়, software engineering-এর একটা fundamental principle।

Credit: Imran Hasan

Address

Dhaka

Alerts

Be the first to know and let us send you an email when Go With Habib posts news and promotions. Your email address will not be used for any other purpose, and you can unsubscribe at any time.

Share