In Go, you can control the visibility of a struct or function using capitalization:
- If a name starts with an uppercase letter, it is exported (public).
- If it starts with a lowercase letter, it is unexported (private to the package).
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:
- We don’t want users of
userService
to be able to create instances ofuserRepo
. - We don’t want
userService
methods to access the database directly.
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:
- You keep the implementation (
userRepo
) private. - You allow other packages to depend on the interface (
UserRepo
), not the implementation.
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:
- Keep your internal logic and implementation details private.
- Expose only the behavior that other packages need.
- Decouple modules.
- Adhere to clean, idiomatic Go design.