This plantpkg
is a command line tool to generate an opinionated Go package structure that solves questions like: How should I organize my Go code?, How do I manage dependencies?
Install the package with
$ go get github.com/theplant/plantpkg
Run this command in terminal and follow prompt to generate a new package
$ plantpkg
Outputs looks like:
$ plantpkg
Your GOPATH: /Users/sunfmin/gopkg
✔ Generate go package: github.com/theplant/helloworld
✔ Service Name: Helloworld
Package "github.com/theplant/helloworld" generated.
cd $GOPATH/github.com/theplant/helloworld && modd; for go to the project directory and run tests.
The package depends on github.com/cortesi/modd
to automatically generate protobuf go structs, and mock package. In order for modd
command to run correctly, You will need to install:
$ brew install protobuf
$ go get -v github.com/cortesi/modd
$ go get -v github.com/golang/mock/mockgen
$ go get -v ./...
The command will generate these files inside the Go package.
.
├── api.go
├── config.go
├── errors.go
├── factory
│ ├── api_test.go
│ └── new.go
├── internal
│ └── impl.go
├── mock
│ └── mock.go
├── modd.conf
├── spec.pb.go
├── spec.proto
└── utils.go
api.go
define the outer facing API interface the package is exposing. which looks like:
type CheckoutService interface {
GiftCardApply(checkoutId string, input *GiftCardInput) (r *GiftCardResult, err error)
ShippingAddressUpdate(checkoutId string, input *AddressInput) (r *Address, err error)
...
}
The params and results are either Go primitive data types, Or protobuf defined data structs.
We intentionally limiting the package exposing API as Go interface for these reasons:
- Easy to read: Limit the places people come to understand the package. People can come to read your
api.go
to know what features the package provide. and don't need to read implementation details in other files of different directories. - Easy to switch: Other packages who use the package can easily change to a different implementation, By switching a different
New
to construct the interface's instance. - Easy to extend: We apply Decoration pattern to the service to wrap more features to a basic implementation.
- Easy to mock: Other packages can easily pass in
mock
package instance of the package, when they don't want to test your packages implementation. - Easy to test: write tests for all functions defined in
api.go
Say with the above CheckoutService
for example, we want to validate the Address before call ShippingAddressUpdate
and after call it we send an email to the user to notify the change, We can do:
type ValidateAndNotifyCheckoutService struct {
basicCheckout CheckoutService
}
func (ch *ValidateAndNotifyCheckoutService) ShippingAddressUpdate(checkoutId string, input *AddressInput) (r *Address, err error) {
err = validateAddress(input)
if err != nil {
return
}
r, err = ch.basicCheckout.ShippingAddressUpdate(checkoutId, input)
if err != nil {
return
}
err = sendNotifyEmail(input.Email)
if err != nil {
return
}
return
}
spec.proto
is the place for the package to define any outer facing data structs that other package depend on this package. It is defined as Google Protocol Buffers, The reason for this is:
- Can easily write an wrapper to expose the package through TCP or HTTP with preferable serialization built-in
- Can embed them into your application API protobuf structs to be part of bigger API definition
- It has pretty good default json format generation by default
- Can still be used as standard Go structs
- All other benefits protobuf provides
factory/new.go
is for you to construct the new instance of the interface defined in api.go
. Where normally you pass in foreign dependencies the package depends, like database connection, configurations, or other plantpkg
created package instances (services).
For example:
func New(db *gorm.DB, cfg *helloworld.Config, emailService email.EmailService, validationService validation.ValidationService) (service *internal.HelloworldImpl) {
...
return
}
The above email.EmailService
, validation.ValidationService
could be yet another plantpkg
generated Go packages interface instance. by formalizing all the packages in plantpkg
style. which gains:
- Easy (Unified) dependencies management
- More clarification of what package do and expose
- Easy to replace to a different implementation
- Easy to extends current implementation with wrapper, and pass them into other packages
- Unified style that the whole team would spend less time to learn new packages
In the above example, say emailService
and validationService
is all optional services, that with them our without them, the package always work. Then it can be implemented this way:
serv := factory.New(db, cfg).EmailService(eserv).ValidationService(vserv)
Which is not passing optional dependencies in New
method, But instead create a separate setter for those optional dependencies and let use set them if they need them.
errors.go
is the place for you to define all the errors that the package can raise. It makes it clear that API could return, So that users of the package could go through them and think about how to handle those errors without looking into implementation details.
utils.go
is for the utility methods related to this package that only depends on the protobuf data structs, which is useful when the data structs are quite complex, and you want people to easy combine, find, calculate values based on variety of API functions returned data structs.
internal/*.go
will be all your internal implementation details source code that you don't need to expose to the people who use your package. You can change the source code inside internal
package from the bottom up, That won't effect other packages depends on the package.
mock
folder is a automatically generated package that mocks api.go
definition, and provide you a mock package by using github.com/golang/mock/gomock
.
In your main application that depends on many plantpkg
packages, and each of them also depends on their own plantpkg
packages. You can setup them in a config
package like this:
func MustGetEmailService() email.EmailService {
return emailFactory.New(...)
}
func MustGetValidationService() validation.ValidationService {
return validationFactory.New(...)
}
func MustGetCheckoutService() checkout.CheckoutService {
return checkoutFactory.New(...)
}
func MustGetValidateAndNotifyCheckoutService() checkout.CheckoutService {
vs := MustGetValidationService()
es := MustGetEmailService()
checkout := MustGetCheckoutService()
return vncheckoutFactory.New(..., checkout, vs, es)
}