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

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 ভালো?
সুবিধা:
পড়তে সহজ - একনজরে বুঝা যায় কোন field এ কী value আছে
Compile-time safety - ভুল field name দিলে compiler error দিবে
No boilerplate - Extra function লেখার দরকার নাই
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:
- 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
}
- Conditional field setting:
p := new(Person)
p.Name = userName
if isPremiumUser {
p.Salary = 100000
} else {
p.Salary = 50000
}
- 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: "রহিম",
}
3. Pointer Literal - সবচেয়ে Popular উপায়
কী এবং কীভাবে?
Pointer literal মানে হলো & দিয়ে struct literal তৈরি করা। এটা একবারেই pointer পাবে এবং value ও set করতে পারবে।
person5 := &Person{
Name: "হাসান মাহমুদ",
Age: 27,
City: "খুলনা",
Salary: 55000.00,
}
// person5 এখন *Person type
কেন এটা এত popular?
বড় সুবিধা:
Memory efficient - Copy হয় না, reference pass হয়
Modification করা easy - Function এ pass করলে original change হবে
Idiomatic Go - এটাই Go community standard
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 লাগবে?
অবশ্যই লাগবে যখন:
- Validation করতে হবে:
func NewEmail(address string) (*Email, error) {
if !isValidEmail(address) {
return nil, errors.New("invalid email format")
}
return &Email{Address: address}, nil
}
- Default value set করতে হবে:
func NewServer(port int) *Server {
return &Server{
Port: port,
Host: "localhost", // Default
MaxConnections: 100, // Default
Timeout: 30 * time.Second, // Default
}
}
- 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
}
- 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:
- 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{}
}
- 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
}
- 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 - পার্থক্য
| Constructor | Factory |
| Single type return করে | Multiple type return করতে পারে |
| Simple initialization | Complex logic থাকতে পারে |
NewPerson() style | CreatePerson() style |
| Direct struct তৈরি | Interface return করতে পারে |
| Predictable output | Runtime 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 ইউজ করো যখন:
- 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))
}
- Immutable behavior চাও:
type Money struct {
Amount float64
Currency string
}
func (m Money) String() string {
return fmt.Sprintf("%.2f %s", m.Amount, m.Currency)
}
- 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 ইউজ করো যখন:
- Modify করতে হবে:
type Counter struct {
Count int
}
func (c *Counter) Increment() {
c.Count++
}
func (c *Counter) Reset() {
c.Count = 0
}
- Large struct:
type LargeData struct {
Items []string
Metadata map[string]interface{}
// ... অনেক field
}
// Copy না করে reference pass করো
func (ld *LargeData) Process() {
// processing logic
}
- 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 তৈরির ৫টি প্রধান উপায়:
Literal Syntax - Simple এবং straightforward
p := Person{Name: "রহিম", Age: 25}new() Function - Pointer চাইলে, zero value দিয়ে
p := new(Person)Pointer Literal - সবচেয়ে popular, একবারে pointer + value
p := &Person{Name: "রহিম", Age: 25}Constructor - Validation আর logic এর জন্য
p, err := NewPerson("রহিম", 25)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 হয়ে যাবে!


