Skip to main content

Command Palette

Search for a command to run...

Go-তে Object (Struct Instance) তৈরির সম্পূর্ণ গাইড

Published
24 min read
Go-তে Object (Struct Instance) তৈরির সম্পূর্ণ গাইড
I

A highly motivated and experienced full-stack developer with a proven track record of developing and deploying web applications. Skilled in a range of programming languages and frameworks, as well as database technologies. Comfortable working in a fast-paced environment and able to adapt to new technologies quickly. A team player who is also able to work independently when required.

Go programming শেখার সময় একটা জিনিস খুব তাড়াতাড়ি বুঝতে হয় - কীভাবে object তৈরি করতে হয়। অন্য language যেমন Java বা Python এ class আছে, কিন্তু Go-তে আছে struct। আর struct এর instance বানানোই হলো object তৈরি করা।

আজকের এই blog এ আমরা দেখব Go-তে object তৈরির সব উপায়, কোনটা কখন ইউজ করবে, এবং receiver function নিয়ে বিস্তারিত আলোচনা। চলো শুরু করা যাক!

একটা Simple Struct দিয়ে শুরু করি

প্রথমে একটা basic struct তৈরি করি যেটা দিয়ে আমরা সব example দেখব:

type Person struct {
    Name    string
    Age     int
    City    string
    Salary  float64
}

এই Person struct এ চারটা field আছে। এখন চলো দেখি এই struct এর instance বানানোর বিভিন্ন উপায়।


1. Basic Literal Syntax - সবচেয়ে সহজ উপায়

কী এবং কীভাবে?

Literal syntax মানে হলো direct curly braces {} ইউজ করে struct এর value দিয়ে দেওয়া। এটা Go-তে সবচেয়ে common এবং readable উপায়।

// পদ্ধতি ১: Field name সহ (Recommended)
person1 := Person{
    Name:   "রহিম উদ্দিন",
    Age:    25,
    City:   "ঢাকা",
    Salary: 50000.00,
}

// পদ্ধতি ২: শুধু value, struct এর field order অনুযায়ী
person2 := Person{"করিম আলী", 30, "চট্টগ্রাম", 60000.00}

// পদ্ধতি ৩: কিছু field দিয়ে, বাকিগুলো zero value হবে
person3 := Person{
    Name: "জামাল হোসেন",
    Age:  28,
    // City আর Salary এর zero value হবে ("" এবং 0.0)
}

কখন এই উপায় ইউজ করবে?

Perfect সময়গুলো:

  • যখন struct টা simple এবং field সংখ্যা কম (৫-৬ টার বেশি না)

  • একবারেই সব data তোমার কাছে ready আছে

  • Quick prototyping বা testing করছো

  • Configuration object বানাচ্ছো যেখানে সব value compile time এ জানা

উদাহরণ - API Response তৈরি:

type APIResponse struct {
    Status  int
    Message string
    Data    interface{}
}

response := APIResponse{
    Status:  200,
    Message: "Success",
    Data:    userList,
}

কেন এই approach ভালো?

সুবিধা:

  1. পড়তে সহজ - একনজরে বুঝা যায় কোন field এ কী value আছে

  2. Compile-time safety - ভুল field name দিলে compiler error দিবে

  3. No boilerplate - Extra function লেখার দরকার নাই

  4. Flexible - যে field দরকার শুধু সেগুলো দিতে পারো

সতর্কতা:

  • Field name ছাড়া value দিলে (পদ্ধতি ২), পরে struct এ নতুন field add করলে code break হতে পারে

  • তাই সবসময় field name সহ লেখা recommended

// ❌ এড়িয়ে চলো - ভবিষ্যতে সমস্যা হতে পারে
p := Person{"রহিম", 25, "ঢাকা", 50000}

// ✅ এভাবে লেখো - safe এবং maintainable
p := Person{
    Name:   "রহিম",
    Age:    25,
    City:   "ঢাকা",
    Salary: 50000,
}

2. new() Function - Pointer এর জন্য

কী এবং কীভাবে?

new() হলো Go-র built-in function যেটা memory allocate করে এবং একটা pointer return করে। এটা struct এর সব field কে তাদের zero value দিয়ে initialize করে।

person4 := new(Person)
// person4 এখন *Person type এর pointer
// সব field এর zero value আছে: Name="", Age=0, City="", Salary=0.0

// এরপর manually value set করতে হবে
person4.Name = "সালমা বেগম"
person4.Age = 32
person4.City = "সিলেট"
person4.Salary = 45000.00

কখন new() ইউজ করবে?

Ideal scenarios:

  1. Step by step initialization করতে হবে:
func processUser(data map[string]interface{}) *Person {
    p := new(Person)

    if name, ok := data["name"].(string); ok {
        p.Name = name
    }

    if age, ok := data["age"].(int); ok {
        p.Age = age
    }

    // আরো processing...
    return p
}
  1. Conditional field setting:
p := new(Person)
p.Name = userName

if isPremiumUser {
    p.Salary = 100000
} else {
    p.Salary = 50000
}
  1. Memory layout নিয়ে control চাইলে:
// একাধিক object একসাথে allocate
users := make([]*Person, 100)
for i := 0; i < 100; i++ {
    users[i] = new(Person)
}

new() vs Literal - কোনটা ভালো?

new(Person)&Person{}
Zero value initialize হয়Custom value দিতে পারো
Pointer return করেPointer return করে
Step by step set করতে হয়একবারে সব দিতে পারো
পুরাতন style (কম দেখা যায়)Modern Go style

সত্যি কথা বলতে, modern Go code এ new() খুব একটা দেখা যায় না। বেশিরভাগ developer &Person{} prefer করে।

// পুরাতন style
p1 := new(Person)
p1.Name = "রহিম"

// Modern style - এটাই recommended
p2 := &Person{
    Name: "রহিম",
}

কী এবং কীভাবে?

Pointer literal মানে হলো & দিয়ে struct literal তৈরি করা। এটা একবারেই pointer পাবে এবং value ও set করতে পারবে।

person5 := &Person{
    Name:   "হাসান মাহমুদ",
    Age:    27,
    City:   "খুলনা",
    Salary: 55000.00,
}
// person5 এখন *Person type

বড় সুবিধা:

  1. Memory efficient - Copy হয় না, reference pass হয়

  2. Modification করা easy - Function এ pass করলে original change হবে

  3. Idiomatic Go - এটাই Go community standard

  4. One-liner - Compact এবং clean

Real-world উদাহরণ

Database operation:

type User struct {
    ID       int
    Username string
    Email    string
}

func CreateUser(username, email string) *User {
    return &User{
        Username: username,
        Email:    email,
    }
}

// ইউজ করা
newUser := CreateUser("rashed", "rashed@example.com")
db.Save(newUser)  // pointer pass হচ্ছে

JSON Response তৈরি:

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func handleError(w http.ResponseWriter, message string) {
    response := &ErrorResponse{
        Code:    400,
        Message: message,
    }
    json.NewEncoder(w).Encode(response)
}

Pointer কখন লাগে আর কখন লাগে না?

Pointer ইউজ করো যখন:

  • Function এ pass করে modify করতে হবে

  • Large struct (memory save করতে)

  • Optional value represent করতে হবে (nil হতে পারে)

  • Interface implement করতে হবে pointer receiver দিয়ে

Pointer লাগে না যখন:

  • Small struct (2-3 field)

  • Immutable data

  • Value semantics চাইলে

// Small struct - pointer না লাগলেও চলে
type Point struct {
    X, Y int
}

p := Point{X: 10, Y: 20}  // Value type যথেষ্ট

// Large struct - pointer better
type Config struct {
    DatabaseURL     string
    RedisURL        string
    AWSAccessKey    string
    // ... অনেক field
}

cfg := &Config{...}  // Pointer ইউজ করো

4. Constructor Function - Professional Approach

কী এবং কেন?

Go-তে official constructor নাই, কিন্তু আমরা convention follow করে New prefix দিয়ে function বানাই। এটা হলো controlled way তে object তৈরি করা।

Basic Constructor Pattern

func NewPerson(name string, age int) *Person {
    return &Person{
        Name: name,
        Age:  age,
        City: "ঢাকা",  // Default value
    }
}

// ইউজ করা
person6 := NewPerson("বিলাল", 29)

Validation সহ Constructor

func NewPerson(name string, age int, city string, salary float64) (*Person, error) {
    // Input validation
    if name == "" {
        return nil, fmt.Errorf("name cannot be empty")
    }

    if age < 18 || age > 100 {
        return nil, fmt.Errorf("age must be between 18 and 100")
    }

    if salary < 0 {
        return nil, fmt.Errorf("salary cannot be negative")
    }

    return &Person{
        Name:   name,
        Age:    age,
        City:   city,
        Salary: salary,
    }, nil
}

// ইউজ করা
person, err := NewPerson("জাহিদ", 25, "রাজশাহী", 48000)
if err != nil {
    log.Fatal(err)
}

কখন Constructor লাগবে?

অবশ্যই লাগবে যখন:

  1. Validation করতে হবে:
func NewEmail(address string) (*Email, error) {
    if !isValidEmail(address) {
        return nil, errors.New("invalid email format")
    }

    return &Email{Address: address}, nil
}
  1. Default value set করতে হবে:
func NewServer(port int) *Server {
    return &Server{
        Port:           port,
        Host:           "localhost",      // Default
        MaxConnections: 100,               // Default
        Timeout:        30 * time.Second,  // Default
    }
}
  1. Complex initialization logic:
func NewDatabase(config DBConfig) (*Database, error) {
    // Connection pool তৈরি
    pool := createConnectionPool(config)

    // Migration চালানো
    if err := runMigrations(pool); err != nil {
        return nil, err
    }

    // Monitoring setup
    monitor := setupMonitoring(config)

    return &Database{
        Pool:    pool,
        Monitor: monitor,
        Config:  config,
    }, nil
}
  1. Dependency injection:
type UserService struct {
    db     *Database
    cache  *Redis
    logger *Logger
}

func NewUserService(db *Database, cache *Redis, logger *Logger) *UserService {
    return &UserService{
        db:     db,
        cache:  cache,
        logger: logger,
    }
}

Constructor Best Practices

✅ Do's:

// 1. সবসময় pointer return করো
func NewPerson(name string) *Person {  // ✅
    return &Person{Name: name}
}

// 2. Error handling করো যেখানে লাগবে
func NewUser(email string) (*User, error) {  // ✅
    if !isValid(email) {
        return nil, errors.New("invalid email")
    }
    return &User{Email: email}, nil
}

// 3. Naming convention follow করো
func NewHTTPServer() *HTTPServer {}  // ✅
func NewPerson() *Person {}          // ✅

❌ Don'ts:

// 1. Value return করো না (special case ছাড়া)
func NewPerson(name string) Person {  // ❌
    return Person{Name: name}
}

// 2. সব parameter required করো না, option pattern ইউজ করো
func NewServer(host string, port int, timeout time.Duration, 
               maxConn int, tls bool, cert string) *Server {  // ❌ Too many params
}

5. Factory Pattern - Advanced Object Creation

কী এবং কেন?

Factory pattern ইউজ করে যখন object তৈরির logic complex হয়, অথবা runtime এ decide করতে হয় কোন type এর object বানাবে।

Simple Factory Example

type Employee interface {
    GetSalary() float64
    GetRole() string
}

type Developer struct {
    Name   string
    Salary float64
}

func (d Developer) GetSalary() float64 { return d.Salary }
func (d Developer) GetRole() string    { return "Developer" }

type Manager struct {
    Name   string
    Salary float64
}

func (m Manager) GetSalary() float64 { return m.Salary }
func (m Manager) GetRole() string    { return "Manager" }

// Factory function
func CreateEmployee(role string, name string) Employee {
    switch role {
    case "developer":
        return &Developer{
            Name:   name,
            Salary: 60000,
        }
    case "manager":
        return &Manager{
            Name:   name,
            Salary: 90000,
        }
    default:
        return &Developer{
            Name:   name,
            Salary: 50000,
        }
    }
}

// ইউজ করা
emp1 := CreateEmployee("developer", "রাশেদ")
emp2 := CreateEmployee("manager", "সাবিনা")

fmt.Println(emp1.GetRole(), emp1.GetSalary())  // Developer 60000
fmt.Println(emp2.GetRole(), emp2.GetSalary())  // Manager 90000

Configuration-based Factory

type DatabaseConfig struct {
    Type     string  // "mysql", "postgres", "mongodb"
    Host     string
    Port     int
    Username string
    Password string
}

type Database interface {
    Connect() error
    Query(sql string) (interface{}, error)
    Close() error
}

func CreateDatabase(config DatabaseConfig) (Database, error) {
    switch config.Type {
    case "mysql":
        return &MySQLDatabase{
            Host:     config.Host,
            Port:     config.Port,
            Username: config.Username,
            Password: config.Password,
        }, nil

    case "postgres":
        return &PostgresDatabase{
            Host:     config.Host,
            Port:     config.Port,
            Username: config.Username,
            Password: config.Password,
        }, nil

    case "mongodb":
        return &MongoDB{
            Host: config.Host,
            Port: config.Port,
        }, nil

    default:
        return nil, fmt.Errorf("unsupported database type: %s", config.Type)
    }
}

কখন Factory Pattern ইউজ করবে?

Perfect scenarios:

  1. Multiple implementation একই interface এর:
type Logger interface {
    Log(message string)
}

func CreateLogger(env string) Logger {
    if env == "production" {
        return &FileLogger{Path: "/var/log/app.log"}
    }
    return &ConsoleLogger{}
}
  1. Complex object graph তৈরি:
func CreateApplication(config AppConfig) (*Application, error) {
    // Database setup
    db, err := CreateDatabase(config.DB)
    if err != nil {
        return nil, err
    }

    // Cache setup
    cache := CreateCache(config.Cache)

    // Logger setup
    logger := CreateLogger(config.Env)

    // Service layer
    userService := NewUserService(db, cache, logger)
    authService := NewAuthService(db, logger)

    return &Application{
        DB:          db,
        Cache:       cache,
        Logger:      logger,
        UserService: userService,
        AuthService: authService,
    }, nil
}
  1. Default values এবং fallback:
func CreatePerson(name string, options map[string]interface{}) *Person {
    p := &Person{Name: name}

    // Age with default
    if age, ok := options["age"].(int); ok {
        p.Age = age
    } else {
        p.Age = 18  // Default age
    }

    // City with default
    if city, ok := options["city"].(string); ok {
        p.City = city
    } else {
        p.City = "ঢাকা"  // Default city
    }

    // Salary calculation based on experience
    if exp, ok := options["experience"].(int); ok {
        p.Salary = calculateSalary(exp)
    } else {
        p.Salary = 30000  // Entry level
    }

    return p
}

func calculateSalary(experience int) float64 {
    baseSalary := 30000.0
    return baseSalary + (float64(experience) * 5000)
}

Factory vs Constructor - পার্থক্য

ConstructorFactory
Single type return করেMultiple type return করতে পারে
Simple initializationComplex logic থাকতে পারে
NewPerson() styleCreatePerson() style
Direct struct তৈরিInterface return করতে পারে
Predictable outputRuntime decision based output

Real-world Factory Example - Payment Processing

type PaymentProcessor interface {
    ProcessPayment(amount float64) error
    ValidatePayment() bool
}

type BkashPayment struct {
    PhoneNumber string
    Amount      float64
}

func (b *BkashPayment) ProcessPayment(amount float64) error {
    b.Amount = amount
    // Bkash API call
    return nil
}

func (b *BkashPayment) ValidatePayment() bool {
    return len(b.PhoneNumber) == 11
}

type CardPayment struct {
    CardNumber string
    CVV        string
    Amount     float64
}

func (c *CardPayment) ProcessPayment(amount float64) error {
    c.Amount = amount
    // Card gateway API call
    return nil
}

func (c *CardPayment) ValidatePayment() bool {
    return len(c.CardNumber) == 16 && len(c.CVV) == 3
}

// Factory function
func CreatePaymentProcessor(method string, details map[string]string) (PaymentProcessor, error) {
    switch method {
    case "bkash":
        phone, ok := details["phone"]
        if !ok {
            return nil, errors.New("phone number required for bKash")
        }
        return &BkashPayment{PhoneNumber: phone}, nil

    case "card":
        cardNum, ok1 := details["card_number"]
        cvv, ok2 := details["cvv"]
        if !ok1 || !ok2 {
            return nil, errors.New("card details required")
        }
        return &CardPayment{
            CardNumber: cardNum,
            CVV:        cvv,
        }, nil

    default:
        return nil, fmt.Errorf("unsupported payment method: %s", method)
    }
}

// Usage
processor, err := CreatePaymentProcessor("bkash", map[string]string{
    "phone": "01712345678",
})
if err != nil {
    log.Fatal(err)
}

if processor.ValidatePayment() {
    processor.ProcessPayment(5000.00)
}

Receiver Function - Detail এ বুঝি

Receiver function হলো Go-তে method define করার উপায়। এটা struct এর সাথে attach থাকে।

Value Receiver vs Pointer Receiver

Value Receiver

type Person struct {
    Name string
    Age  int
}

// Value receiver - copy তে কাজ করে
func (p Person) GetInfo() string {
    return fmt.Sprintf("%s is %d years old", p.Name, p.Age)
}

func (p Person) IncrementAge() {
    p.Age++  // এটা শুধু copy তে change হবে, original unchanged
}

// ইউজ করা
person := Person{Name: "রহিম", Age: 25}
fmt.Println(person.GetInfo())  // রহিম is 25 years old

person.IncrementAge()
fmt.Println(person.Age)  // Still 25! কারণ copy modify হয়েছে

Pointer Receiver

// Pointer receiver - original এ কাজ করে
func (p *Person) Birthday() {
    p.Age++  // Original object modify হবে
}

func (p *Person) UpdateName(newName string) {
    p.Name = newName  // Original change হবে
}

// ইউজ করা
person := &Person{Name: "করিম", Age: 30}
person.Birthday()
fmt.Println(person.Age)  // 31 - changed!

person.UpdateName("করিম আহমেদ")
fmt.Println(person.Name)  // করিম আহমেদ - changed!

কখন কোনটা ইউজ করবে?

Value Receiver ইউজ করো যখন:

  1. Small struct যেটা copy করা cheap:
type Point struct {
    X, Y int
}

func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
  1. Immutable behavior চাও:
type Money struct {
    Amount   float64
    Currency string
}

func (m Money) String() string {
    return fmt.Sprintf("%.2f %s", m.Amount, m.Currency)
}
  1. Read-only operation:
type Config struct {
    Host string
    Port int
}

func (c Config) GetURL() string {
    return fmt.Sprintf("http://%s:%d", c.Host, c.Port)
}

Pointer Receiver ইউজ করো যখন:

  1. Modify করতে হবে:
type Counter struct {
    Count int
}

func (c *Counter) Increment() {
    c.Count++
}

func (c *Counter) Reset() {
    c.Count = 0
}
  1. Large struct:
type LargeData struct {
    Items []string
    Metadata map[string]interface{}
    // ... অনেক field
}

// Copy না করে reference pass করো
func (ld *LargeData) Process() {
    // processing logic
}
  1. Consistency maintain করতে:
type Database struct {
    conn *sql.DB
}

// সব method এ pointer receiver ইউজ করো
func (db *Database) Query(sql string) error {
    // ...
}

func (db *Database) Close() error {
    // ...
}

Method Chaining

Pointer receiver দিয়ে method chaining করা যায়:

type Person struct {
    Name   string
    Age    int
    City   string
    Salary float64
}

func (p *Person) SetName(name string) *Person {
    p.Name = name
    return p
}

func (p *Person) SetAge(age int) *Person {
    p.Age = age
    return p
}

func (p *Person) SetCity(city string) *Person {
    p.City = city
    return p
}

// Chaining করা
person := &Person{}
person.SetName("রহিম").
       SetAge(25).
       SetCity("ঢাকা")

fmt.Println(person)  // &{রহিম 25 ঢাকা 0}

Complete Real-World Example

চলো একটা complete example দেখি যেখানে সব technique একসাথে ইউজ হয়েছে:

package main

import (
    "errors"
    "fmt"
    "time"
)

// Base struct
type User struct {
    ID        int
    Username  string
    Email     string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
    IsActive  bool
}

// Constructor with validation
func NewUser(username, email, password string) (*User, error) {
    if username == "" {
        return nil, errors.New("username cannot be empty")
    }

    if len(password) < 6 {
        return nil, errors.New("password must be at least 6 characters")
    }

    now := time.Now()
    return &User{
        Username:  username,
        Email:     email,
        Password:  hashPassword(password),
        CreatedAt: now,
        UpdatedAt: now,
        IsActive:  true,
    }, nil
}

// Value receiver - read only
func (u User) GetDisplayName() string {
    return fmt.Sprintf("@%s", u.Username)
}

func (u User) IsValid() bool {
    return u.Username != "" && u.Email != "" && u.IsActive
}

// Pointer receiver - modify
func (u *User) Activate() {
    u.IsActive = true
    u.UpdatedAt = time.Now()
}

func (u *User) Deactivate() {
    u.IsActive = false
    u.UpdatedAt = time.Now()
}

func (u *User) UpdateEmail(newEmail string) error {
    if newEmail == "" {
        return errors.New("email cannot be empty")
    }
    u.Email = newEmail
    u.UpdatedAt = time.Now()
    return nil
}

// Factory pattern for different user types
type UserType string

const (
    RegularUser UserType = "regular"
    AdminUser   UserType = "admin"
    GuestUser   UserType = "guest"
)

func CreateUserByType(userType UserType, username, email string) (*User, error) {
    var password string
    var isActive bool

    switch userType {
    case RegularUser:
        password = "default123"
        isActive = false  // Email verification লাগবে

    case AdminUser:
        password = "admin@secure"
        isActive = true

    case GuestUser:
        password = "guest"
        isActive = true
        username = "guest_" + username

    default:
        return nil, fmt.Errorf("unknown user type: %s", userType)
    }

    user, err := NewUser(username, email, password)
    if err != nil {
        return nil, err
    }

    user.IsActive = isActive
    return user, nil
}

// Helper function
func hashPassword(password string) string {
    // এখানে real hashing হবে (bcrypt, etc.)
    return "hashed_" + password
}

func main() {
    // Constructor ইউজ করে
    user1, err := NewUser("rashed", "rashed@example.com", "secret123")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("User 1:", user1.GetDisplayName())

    // Factory ইউজ করে
    admin, err := CreateUserByType(AdminUser, "admin", "admin@example.com")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Admin:", admin.GetDisplayName(), "Active:", admin.IsActive)

    // Pointer literal
    user2 := &User{
        Username: "karim",
        Email:    "karim@example.com",
        IsActive: true,
    }

    // Method calling
    user2.Deactivate()
    fmt.Println("User 2 active:", user2.IsActive)  // false

    user2.Activate()
    fmt.Println("User 2 active:", user2.IsActive)  // true

    // Update করা
    err = user2.UpdateEmail("karim.new@example.com")
    if err != nil {
        fmt.Println("Error:", err)
    }
    fmt.Println("Updated email:", user2.Email)
}

Best Practices এবং Common Mistakes

✅ Best Practices

1. Constructor Naming Convention

Go community তে একটা standard convention আছে constructor function এর naming এর জন্য:

// ✅ Single struct এর জন্য - New prefix
func NewPerson(name string) *Person {
    return &Person{Name: name}
}

// ✅ Package এ multiple type থাকলে - NewTypeName pattern
func NewHTTPClient() *HTTPClient {}
func NewHTTPServer() *HTTPServer {}

// ✅ Factory function এর জন্য - Create prefix
func CreateEmployee(role string) Employee {
    // ...
}

// ❌ এড়িয়ে চলো
func MakePerson() *Person {}      // Inconsistent
func PersonConstructor() *Person {} // Too verbose
func GetNewPerson() *Person {}     // Confusing

2. Error Handling in Constructors

Constructor থেকে সবসময় error return করো যদি validation লাগে:

// ✅ Good - error handling আছে
func NewEmail(address string) (*Email, error) {
    if !isValidEmail(address) {
        return nil, fmt.Errorf("invalid email: %s", address)
    }
    return &Email{Address: address}, nil
}

// ✅ Alternative - panic করতে পারো critical error এর জন্য
func MustNewEmail(address string) *Email {
    email, err := NewEmail(address)
    if err != nil {
        panic(err)
    }
    return email
}

// Usage
email1, err := NewEmail("test@example.com")
if err != nil {
    log.Fatal(err)
}

// যখন confident যে error হবে না
email2 := MustNewEmail("admin@system.com")

3. Option Pattern - Flexible Construction

যখন অনেক optional parameter থাকে, option pattern ইউজ করো:

type Server struct {
    Host           string
    Port           int
    Timeout        time.Duration
    MaxConnections int
    TLSEnabled     bool
}

// Option function type
type ServerOption func(*Server)

// Option functions
func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.Host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.Port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.Timeout = timeout
    }
}

func WithTLS() ServerOption {
    return func(s *Server) {
        s.TLSEnabled = true
    }
}

// Constructor যেটা options নেয়
func NewServer(options ...ServerOption) *Server {
    // Default values
    server := &Server{
        Host:           "localhost",
        Port:           8080,
        Timeout:        30 * time.Second,
        MaxConnections: 100,
        TLSEnabled:     false,
    }

    // Apply options
    for _, option := range options {
        option(server)
    }

    return server
}

// Usage - খুব flexible!
server1 := NewServer()  // সব default value

server2 := NewServer(
    WithHost("0.0.0.0"),
    WithPort(9000),
)

server3 := NewServer(
    WithHost("api.example.com"),
    WithPort(443),
    WithTimeout(60 * time.Second),
    WithTLS(),
)

এই pattern এর সুবিধা:

  • Readable এবং self-documenting

  • Backward compatible - নতুন option add করলে existing code break হয় না

  • Default value automatically handle হয়

  • শুধু যা লাগে তাই দিতে হয়

4. Builder Pattern - Complex Objects এর জন্য

যখন object তৈরি করতে অনেক step লাগে:

type QueryBuilder struct {
    table      string
    columns    []string
    conditions []string
    orderBy    string
    limit      int
}

func NewQueryBuilder(table string) *QueryBuilder {
    return &QueryBuilder{
        table:   table,
        columns: []string{"*"},
    }
}

func (qb *QueryBuilder) Select(columns ...string) *QueryBuilder {
    qb.columns = columns
    return qb
}

func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
    qb.conditions = append(qb.conditions, condition)
    return qb
}

func (qb *QueryBuilder) OrderBy(column string) *QueryBuilder {
    qb.orderBy = column
    return qb
}

func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
    qb.limit = n
    return qb
}

func (qb *QueryBuilder) Build() string {
    query := "SELECT " + strings.Join(qb.columns, ", ")
    query += " FROM " + qb.table

    if len(qb.conditions) > 0 {
        query += " WHERE " + strings.Join(qb.conditions, " AND ")
    }

    if qb.orderBy != "" {
        query += " ORDER BY " + qb.orderBy
    }

    if qb.limit > 0 {
        query += fmt.Sprintf(" LIMIT %d", qb.limit)
    }

    return query
}

// Usage - method chaining
query := NewQueryBuilder("users").
    Select("id", "name", "email").
    Where("age > 18").
    Where("city = 'ঢাকা'").
    OrderBy("name").
    Limit(10).
    Build()

fmt.Println(query)
// SELECT id, name, email FROM users WHERE age > 18 AND city = 'ঢাকা' ORDER BY name LIMIT 10

5. Struct Embedding - Composition

Go তে inheritance নাই, কিন্তু composition করতে পারো:

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    Name string
    Age  int
    Address  // Embedded - Person এর সব method এ Address পাবে
}

type Employee struct {
    Person   // Embedded - Employee এ Person এর সব field আছে
    EmployeeID string
    Department string
    Salary     float64
}

// Constructor for Employee
func NewEmployee(name string, age int, city string, empID string, dept string) *Employee {
    return &Employee{
        Person: Person{
            Name: name,
            Age:  age,
            Address: Address{
                City: city,
            },
        },
        EmployeeID: empID,
        Department: dept,
    }
}

// Usage
emp := NewEmployee("রহিম", 30, "ঢাকা", "EMP001", "IT")
fmt.Println(emp.Name)    // Embedded field থেকে direct access
fmt.Println(emp.City)    // Nested embedded field
fmt.Println(emp.Department)

❌ Common Mistakes এবং কীভাবে এড়াবে

Mistake 1: Zero Value Confusion

type Config struct {
    Port    int
    Enabled bool
}

// ❌ সমস্যা - zero value distinguish করতে পারবে না
cfg := Config{}
fmt.Println(cfg.Port)    // 0 - এটা কি intentional না default?
fmt.Println(cfg.Enabled) // false - user চায়নি নাকি default?

// ✅ Solution 1 - Pointer ইউজ করো optional field এর জন্য
type Config struct {
    Port    *int  // nil মানে not set
    Enabled *bool // nil মানে not set
}

// ✅ Solution 2 - Constructor এ explicit value দাও
func NewConfig() *Config {
    return &Config{
        Port:    8080,  // Clear default
        Enabled: true,  // Clear default
    }
}

Mistake 2: Returning Value instead of Pointer

// ❌ Problem - large struct copy হচ্ছে
type LargeData struct {
    Items []string
    Data  map[string]interface{}
    // ... অনেক field
}

func ProcessData() LargeData {  // Value return - expensive!
    data := LargeData{
        Items: make([]string, 1000),
        Data:  make(map[string]interface{}),
    }
    return data  // পুরো struct copy হবে
}

// ✅ Solution - pointer return করো
func ProcessData() *LargeData {
    return &LargeData{
        Items: make([]string, 1000),
        Data:  make(map[string]interface{}),
    }
}

Mistake 3: Pointer to Pointer Confusion

// ❌ Unnecessarily complex
func NewPerson(name string) **Person {
    p := &Person{Name: name}
    return &p  // pointer to pointer - কেন?!
}

// ✅ Simple pointer যথেষ্ট
func NewPerson(name string) *Person {
    return &Person{Name: name}
}

// ℹ️ Double pointer শুধু তখনই লাগে যখন pointer modify করতে হবে
func ModifyPointer(p **Person) {
    *p = &Person{Name: "New Person"}
}

Mistake 4: Mixed Receiver Types

// ❌ Inconsistent - কিছু value, কিছু pointer
type Counter struct {
    Count int
}

func (c Counter) Increment() {   // Value receiver
    c.Count++  // কাজ করবে না!
}

func (c *Counter) Reset() {      // Pointer receiver
    c.Count = 0  // কাজ করবে
}

// ✅ Consistent - সব pointer receiver
type Counter struct {
    Count int
}

func (c *Counter) Increment() {
    c.Count++
}

func (c *Counter) Reset() {
    c.Count = 0
}

func (c *Counter) GetCount() int {  // Read-only এও pointer ইউজ করা better consistency এর জন্য
    return c.Count
}

Mistake 5: Not Handling nil Pointers

// ❌ Panic করবে যদি nil পাঠানো হয়
func PrintPerson(p *Person) {
    fmt.Println(p.Name)  // p nil হলে panic!
}

// ✅ nil check করো
func PrintPerson(p *Person) {
    if p == nil {
        fmt.Println("No person provided")
        return
    }
    fmt.Println(p.Name)
}

// ✅ অথবা method এ nil check
func (p *Person) GetName() string {
    if p == nil {
        return "Unknown"
    }
    return p.Name
}

Performance Considerations

Memory Allocation

// Benchmark করে দেখি কোনটা fast

type SmallStruct struct {
    X, Y int
}

type LargeStruct struct {
    Data [1000]int
}

// Value vs Pointer for small struct
func BenchmarkSmallValue(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := SmallStruct{X: 1, Y: 2}  // Stack allocation - fast
        _ = s
    }
}

func BenchmarkSmallPointer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := &SmallStruct{X: 1, Y: 2}  // Heap allocation - slower
        _ = s
    }
}

// Value vs Pointer for large struct
func BenchmarkLargeValue(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := LargeStruct{}  // Stack তে copy - very slow!
        _ = s
    }
}

func BenchmarkLargePointer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := &LargeStruct{}  // Heap তে reference - fast
        _ = s
    }
}

Rule of thumb:

  • Small struct (< 100 bytes): Value type OK

  • Large struct (> 100 bytes): Pointer type better

  • Modify করতে হলে: Always pointer

Stack vs Heap

Go compiler automatically decide করে কোথায় allocate হবে:

func CreateOnStack() SmallStruct {
    return SmallStruct{X: 1, Y: 2}  // Stack এ allocate
}

func CreateOnHeap() *SmallStruct {
    return &SmallStruct{X: 1, Y: 2}  // Heap এ allocate
}

// Escape analysis দেখতে
// go build -gcflags="-m" main.go

Real-World Patterns

Pattern 1: Repository Pattern

type User struct {
    ID    int
    Name  string
    Email string
}

type UserRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *User) error {
    query := "INSERT INTO users (name, email) VALUES (?, ?)"
    result, err := r.db.Exec(query, user.Name, user.Email)
    if err != nil {
        return err
    }

    id, _ := result.LastInsertId()
    user.ID = int(id)
    return nil
}

func (r *UserRepository) FindByID(id int) (*User, error) {
    user := &User{}
    query := "SELECT id, name, email FROM users WHERE id = ?"

    err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return nil, err
    }

    return user, nil
}

func (r *UserRepository) Update(user *User) error {
    query := "UPDATE users SET name = ?, email = ? WHERE id = ?"
    _, err := r.db.Exec(query, user.Name, user.Email, user.ID)
    return err
}

Pattern 2: Service Layer with Dependency Injection

type UserService struct {
    repo   *UserRepository
    cache  *RedisCache
    logger *Logger
}

func NewUserService(repo *UserRepository, cache *RedisCache, logger *Logger) *UserService {
    return &UserService{
        repo:   repo,
        cache:  cache,
        logger: logger,
    }
}

func (s *UserService) GetUser(id int) (*User, error) {
    // Try cache first
    if user, err := s.cache.Get(fmt.Sprintf("user:%d", id)); err == nil {
        s.logger.Info("Cache hit for user", id)
        return user.(*User), nil
    }

    // Not in cache, get from DB
    user, err := s.repo.FindByID(id)
    if err != nil {
        s.logger.Error("Failed to get user", err)
        return nil, err
    }

    // Store in cache
    s.cache.Set(fmt.Sprintf("user:%d", id), user, 10*time.Minute)

    return user, nil
}

func (s *UserService) CreateUser(name, email string) (*User, error) {
    user := &User{
        Name:  name,
        Email: email,
    }

    if err := s.repo.Create(user); err != nil {
        s.logger.Error("Failed to create user", err)
        return nil, err
    }

    s.logger.Info("User created", user.ID)
    return user, nil
}

Pattern 3: Singleton Pattern (Rarely used but good to know)

type Database struct {
    conn *sql.DB
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        conn, err := sql.Open("mysql", "connection_string")
        if err != nil {
            panic(err)
        }
        instance = &Database{conn: conn}
    })
    return instance
}

// Usage
db1 := GetDatabase()
db2 := GetDatabase()
// db1 এবং db2 same instance

Testing এ Object Creation

Table-Driven Tests

func TestNewPerson(t *testing.T) {
    tests := []struct {
        name        string
        inputName   string
        inputAge    int
        expectError bool
    }{
        {
            name:        "Valid person",
            inputName:   "রহিম",
            inputAge:    25,
            expectError: false,
        },
        {
            name:        "Empty name",
            inputName:   "",
            inputAge:    25,
            expectError: true,
        },
        {
            name:        "Negative age",
            inputName:   "করিম",
            inputAge:    -5,
            expectError: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            person, err := NewPerson(tt.inputName, tt.inputAge)

            if tt.expectError {
                if err == nil {
                    t.Error("Expected error but got nil")
                }
                return
            }

            if err != nil {
                t.Errorf("Unexpected error: %v", err)
            }

            if person.Name != tt.inputName {
                t.Errorf("Expected name %s, got %s", tt.inputName, person.Name)
            }
        })
    }
}

Mock Objects

// Interface define করো
type EmailSender interface {
    Send(to, subject, body string) error
}

// Real implementation
type SMTPSender struct {
    host string
    port int
}

func (s *SMTPSender) Send(to, subject, body string) error {
    // Real email sending logic
    return nil
}

// Mock implementation for testing
type MockEmailSender struct {
    SentEmails []string
}

func (m *MockEmailSender) Send(to, subject, body string) error {
    m.SentEmails = append(m.SentEmails, to)
    return nil
}

// Service যেটা EmailSender ইউজ করে
type NotificationService struct {
    emailSender EmailSender
}

func NewNotificationService(sender EmailSender) *NotificationService {
    return &NotificationService{emailSender: sender}
}

func (n *NotificationService) NotifyUser(email, message string) error {
    return n.emailSender.Send(email, "Notification", message)
}

// Test এ mock ইউজ করা
func TestNotificationService(t *testing.T) {
    mock := &MockEmailSender{}
    service := NewNotificationService(mock)

    err := service.NotifyUser("test@example.com", "Hello")
    if err != nil {
        t.Fatal(err)
    }

    if len(mock.SentEmails) != 1 {
        t.Errorf("Expected 1 email, got %d", len(mock.SentEmails))
    }
}

সারসংক্ষেপ - কী শিখলাম?

Object তৈরির ৫টি প্রধান উপায়:

  1. Literal Syntax - Simple এবং straightforward

     p := Person{Name: "রহিম", Age: 25}
    
  2. new() Function - Pointer চাইলে, zero value দিয়ে

     p := new(Person)
    
  3. Pointer Literal - সবচেয়ে popular, একবারে pointer + value

     p := &Person{Name: "রহিম", Age: 25}
    
  4. Constructor - Validation আর logic এর জন্য

     p, err := NewPerson("রহিম", 25)
    
  5. Factory Pattern - Complex creation logic এর জন্য

     p := CreatePerson("developer", "রহিম")
    

Receiver Function:

  • Value Receiver - Read-only, small struct

  • Pointer Receiver - Modify করতে হলে, large struct

মনে রাখার মতো বিষয়:

Do:

  • Constructor থেকে pointer return করো

  • Consistent receiver type ইউজ করো

  • Error handling করো যেখানে লাগবে

  • Option pattern ইউজ করো অনেক parameter থাকলে

  • Interface ইউজ করো flexibility এর জন্য

Don't:

  • Mixed receiver type ইউজ করো না

  • Nil pointer check ভুলে যেও না

  • Small struct এর জন্য unnecessary pointer ইউজ করো না

  • Constructor naming convention break করো না


শেষ কথা

Go-তে object তৈরি করা অন্য language এর চেয়ে একটু আলাদা, কিন্তু এটা শিখে ফেললে অনেক clear হয়ে যাবে। মনে রাখবে:

  • Simple case এ simple approach - Literal syntax ই যথেষ্ট

  • Validation লাগলে constructor - Clean এবং safe

  • Complex logic থাকলে factory - Flexible এবং maintainable

  • Pointer receiver সবসময় না - যেখানে দরকার সেখানেই

Practice করো, experiment করো, আর নিজের code review করো। Go community এর open source project গুলো দেখো - কীভাবে তারা struct আর receiver ইউজ করে। এভাবেই expert হয়ে যাবে!

More from this blog

Low Level Design: গভীর থেকে বোঝা এবং আয়ত্ত করা

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

Oct 15, 202520 min read71
Low Level Design: গভীর থেকে বোঝা এবং আয়ত্ত করা

Go-তে Interface কীভাবে Code Decouple করে?

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

Oct 14, 202520 min read5
Go-তে Interface কীভাবে Code Decouple করে?

তোমার Project-এ Coupled Code কীভাবে খুঁজে বের করবে?

Coupled code খোঁজা মানে হচ্ছে তোমার codebase-এ এমন জায়গা খুঁজে বের করা যেখানে একটা অংশ আরেকটার উপর বেশি depend করছে। এটা একটা detective work — তুমি clue খুঁজবে, pattern দেখবে, এবং সমস্যা চিহ্নিত করবে। চলো step by step শিখি কীভাবে এটা করতে হয়। কেন ...

Oct 14, 202510 min read2
I

Imran Hasan

61 posts

Full-stack developer with experience in developing and managing web applications. Skilled in React, Node.js, HTML, CSS, and JavaScript. Experience in managing website hosting and security.