Skip to content

Encapsulation in Go: Keeping Structs Private While Exposing Functionality

Published: at 03:30 AM
|

In Go, you can control the visibility of a struct or function using capitalization:

Let’s consider the following project structure:

.
├── go.mod
├── main.go
├── repository
│   └── user_repo.go
└── service
    └── user_service.go

Suppose we have a userRepo in the repository package that we want to use inside userService. However:

To solve this, we can keep the userRepo struct private and expose it only through a constructor function:

// repository/user_repo.go
package repository

type userRepo struct {
    db *driver.Database
}

func NewUserRepo() userRepo {
    dbInstance := /* initialize your DB here */
    return userRepo{
        db: dbInstance,
    }
}

func (ur userRepo) Get(ctx context.Context, userId string) (*User, error) {
    // Implementation here...
}

This works great as long as you don’t need to refer to its type outside the package:

func example() {
    repo := repository.NewUserRepo()
    user, err := repo.Get(context.Background(), "123")
    // ...
}

The Problem

Now suppose you’re building a userService that stores a userRepo as a field:

// service/user_service.go
package service

type userService struct {
    userRepo repository.userRepo // ❌ This won't compile — userRepo is private!
}

func NewUserService() userService {
    return userService{
        userRepo: repository.NewUserRepo(), // ❌ This also won't compile
    }
}

You can’t use the unexported userRepo type outside of the repository package—even if you’re accessing it correctly via a constructor.


The Solution: Use Interfaces

The idiomatic Go solution is to define an interface that exposes only the behavior you need. This way:

repository/user_repo.go

package repository

type UserRepo interface {
    Get(ctx context.Context, userId string) (*User, error)
}

type userRepo struct {
    db *driver.Database
}

func NewUserRepo() UserRepo {
    dbInstance := /* initialize your DB here */
    return userRepo{
        db: dbInstance,
    }
}

func (ur userRepo) Get(ctx context.Context, userId string) (*User, error) {
    // Implementation...
}

service/user_service.go

package service

import "your_project/repository"

type userService struct {
    userRepo repository.UserRepo
}

func NewUserService() userService {
    return userService{
        userRepo: repository.NewUserRepo(),
    }
}

func (svc userService) DoSomething(ctx context.Context, userId string) (*SomeOutput, error) {
    user, err := svc.userRepo.Get(ctx, userId)
    if err != nil {
        return nil, err
    }

    // Perform additional logic...

    return &SomeOutput{/*...*/}, nil
}

Conclusion

By using interfaces in Go, you can: