Below is a set of instructions and conventions for building a “goo”-based application. This illustrates how to bring together config, logging, database connections, and graceful shutdown via goo
’s helpers. The code references below come directly from this repository’s structure and from the sample examples/cli
app.
Ask clarifications if necessary
- ask what the golang package import path should be
- ask the user what the package name should be called
Writing the code.
- Define config, database, args parsing, log, as described below in the example.
- Application code should be put in the named package.
- Define package main in
main.go
that callsInitMain()
. - If database is used, ensure that the sqlite3 driver is included.
import _ "github.com/mattn/go-sqlite3"
- Database migrations is embedded in the code as
[]goo.Migration
, usinggoo.DBMigrator
.
- Ensure the wire cli tool is installed:
import _ github.com/google/wire/cmd/wire
- Create a standard
.gitignore
. - Put temporary artifacts (e.g. sqlite db and built binaries) in .gitignore.
Style guide for writing the app:
- Use
goo/fetch
to implement JSON API. - Put sqlx code in a
AppStore
struct, rather than sprinkling SQL all over the app. - Use sqlx effectively by taking advantage of its support to scan into slices and structs.
- Take advantage of wire DI. For a singleton struct type with dependencies, write
ProvideAppStore
as a provider, and put that in theWires
set.
Building the app:
- Run
make wire
after writing code to generate the dependency injection. - Run
go build
to ensure that your code works. - Try to fix errors.
Important: Write the code by following the examples below.
<file_map>
└── mycli # package mycli
├── cmd # `package main` for the cli
│ └── main.go
├── app.go
├── cfg.toml
├── Makefile
├── modd.conf
├── wire_gen.go
└── wire.go
</file_map>
<file_contents> File: examples/cli/cmd/main.go
package main
import (
"github.com/hayeah/goo/examples/cli"
)
func main() {
mainfn, err := cli.InitMain()
if err != nil {
panic(err)
}
mainfn()
}
File: examples/cli/cfg.toml
[Database]
Dialect = "sqlite3"
DSN = "mydb.sqlite3"
MigrationsPath = "./migrations"
[Logging]
LogLevel = "DEBUG"
# LogFormat = "console"
[Echo]
Listen = ":8000"
File: examples/cli/modd.conf
*.go cfg.toml {
daemon: CONFIG_FILE=cfg.toml go run ./cmd
}
File: examples/cli/Makefile
.PHONY: wire run dev
wire:
go run github.com/google/wire/cmd/wire .
# make run ARGS="somefile.go --flag1 --flag2"
run:
CONFIG_FILE=cfg.toml go run ./cmd $(ARGS)
dev:
CONFIG_FILE=cfg.toml go run github.com/cortesi/modd/cmd/modd@latest
File: examples/cli/wire.go
//go:build wireinject
package cli
import (
"github.com/google/wire"
"github.com/hayeah/goo"
)
func InitMain() (goo.Main, error) {
panic(wire.Build(Wires))
}
File: examples/cli/wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package cli
import (
"github.com/hayeah/goo"
)
// Injectors from wire.go:
func InitMain() (goo.Main, error) {
config, err := ProvideConfig()
if err != nil {
return nil, err
}
gooConfig, err := ProvideGooConfig(config)
if err != nil {
return nil, err
}
logger, err := goo.ProvideSlog(gooConfig)
if err != nil {
return nil, err
}
args, err := ProvideArgs()
if err != nil {
return nil, err
}
shutdownContext, err := goo.ProvideShutdownContext(logger)
if err != nil {
return nil, err
}
db, err := goo.ProvideSQLX(gooConfig, shutdownContext, logger)
if err != nil {
return nil, err
}
dbMigrator := goo.ProvideDBMigrator(db, logger)
app := &App{
Args: args,
Config: config,
Shutdown: shutdownContext,
DB: db,
Migrator: dbMigrator,
}
main := goo.ProvideMain(logger, app, shutdownContext)
return main, nil
}
File: examples/cli/app.go
package cli
import (
"fmt"
"log"
"github.com/google/wire"
"github.com/hayeah/goo"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" // Import SQLite driver
)
// collect all the necessary providers
var Wires = wire.NewSet(
goo.Wires,
// provide the base config for goo library
ProvideGooConfig,
// app specific providers
ProvideConfig,
ProvideArgs,
// example: provide a goo.Runner interface for Main function, using a provider function
// ProvideRunner,
// example: provide a goo.Runner interface for Main function, by using interface binding
wire.Struct(new(App), "*"),
wire.Bind(new(goo.Runner), new(*App)),
)
func ProvideConfig() (*Config, error) {
cfg, err := goo.ParseConfig[Config]("")
if err != nil {
return nil, err
}
return cfg, nil
}
func ProvideGooConfig(cfg *Config) (*goo.Config, error) {
return &cfg.Config, nil
}
// ProvideArgs parses cli args
func ProvideArgs() (*Args, error) {
return goo.ParseArgs[Args]()
}
func ProvideRunner() (goo.Runner, error) {
return &App{}, nil
}
type Config struct {
goo.Config
OpenAI OpenAIConfig
}
type OpenAIConfig struct {
APIKey string
}
type CheckoutCmd struct {
Branch string `arg:"positional"`
Track bool `arg:"-t"`
}
type CommitCmd struct {
All bool `arg:"-a"`
Message string `arg:"-m"`
}
type PushCmd struct {
Remote string `arg:"positional"`
Branch string `arg:"positional"`
SetUpstream bool `arg:"-u"`
}
type Args struct {
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
Commit *CommitCmd `arg:"subcommand:commit"`
Push *PushCmd `arg:"subcommand:push"`
}
type App struct {
Args *Args
Config *Config
Shutdown *goo.ShutdownContext
DB *sqlx.DB
Migrator *goo.DBMigrator
}
func (app *App) Run() error {
err := app.Migrator.Up([]goo.Migration{
{
Name: "create_users_table",
Up: `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
`,
},
})
if err != nil {
return err
}
args := app.Args
switch {
case args.Checkout != nil:
log.Printf("checkout %v", args.Checkout)
case args.Commit != nil:
log.Printf("commit %v", args.Commit)
case args.Push != nil:
log.Printf("push %v", args.Push)
default:
return fmt.Errorf("unknown command")
}
return nil
}
</file_contents>
- This is an example of using the
goo/fetch
library to request a JSON API. - Only define types if they are relevant to the app.
- Avoid types defined only to map the JSON API by using gjson to get the data you.
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
func (app *App) callAnthropicAPI(chatID int64, userMessage string) (string, error) {
// Get previous messages for context
messages, err := app.ChatStore.GetMessages(chatID)
if err != nil {
return "", fmt.Errorf("failed to get previous messages: %w", err)
}
// Add the current user message
messages = append(messages, Message{
Role: "user",
Content: userMessage,
})
// Call Anthropic API
opts := &fetch.Options{
BaseURL: "https://api.anthropic.com",
Header: http.Header{
"Content-Type": {"application/json"},
"x-api-key": {app.Config.Anthropic.APIKey},
"anthropic-version": {"2023-06-01"},
},
Logger: app.Logger,
}
resp, err := opts.JSON("POST", "/v1/messages", &fetch.Options{
Body: `{
"model": "claude-3-7-sonnet-20250219",
"messages": {{Messages}},
"max_tokens": 1024
}`,
BodyParams: map[string]any{
"Messages": messages,
},
})
if err != nil {
return "", fmt.Errorf("API request failed: %w", err)
}
// Extract the response content
content := resp.Get("content.0.text").String()
return content, nil
}
main.go
callsInitMain()
, gets agoo.Main
function, and calls that function.InitMain()
is defined inwire.go
using Google’s Wire for dependency injection.Wires = wire.NewSet(...)
that enumerates all providers from thegoo
library (goo.Wires
) plus your own custom providers.- The
App
struct inapp.go
implementsgoo.Runner
. InsideApp.Run()
, you can do any final initialization such as database migrations.
goo.Config
is the baseline config for databases, logging, and echo server. You can embed it inside your own config struct if you want to add additional fields. For example:
// In app.go (or in a dedicated config.go):
package cli
import (
"github.com/hayeah/goo"
)
type Config struct {
goo.Config // embed goo’s config
OpenAI struct {
APIKey string
}
// ... other fields
}
Set environment variables or a config file. For example:
Load config as file:
# The file extension supported are: .toml, .json
CONFIG_FILE=cfg.toml go run ./cmd
Load config as JSON string
CONFIG_JSON=`cat cfg.json` go run ./cmd
Load config as TOML string
CONFIG_JSON=`cat cfg.json` go run ./cmd
The application struct must implement goo.Runner, which is a single Run() error
method.
Example (from examples/cli/app.go
):
type App struct {
Args *Args // For parsed CLI arguments
Config *Config // For loaded config
Shutdown *goo.ShutdownContext
DB *sqlx.DB
Migrator *goo.DBMigrator
}
func (app *App) Run() error {
// Example: run migrations
err := app.Migrator.Up([]goo.Migration{
{
Name: "create_users_table",
Up: `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
`,
},
// ... more migrations
})
if err != nil {
return err
}
// Then do the actual logic, e.g. handle CLI subcommands
switch {
case app.Args.Checkout != nil:
// ...
case app.Args.Commit != nil:
// ...
default:
return fmt.Errorf("unknown command")
}
return nil
}
App
’s fields are wired (injected) by the providers described below.
You typically collect all providers in a Wires
variable:
func ProvideConfig() (*Config, error) {
// Parse environment + file
cfg, err := goo.ParseConfig[Config]("") // prefix = ""
if err != nil {
return nil, err
}
return cfg, nil
}
func ProvideGooConfig(cfg *Config) (*goo.Config, error) {
// The goo library specifically needs a pointer to the embedded goo.Config
return &cfg.Config, nil
}
func ProvideArgs() (*Args, error) {
// CLI argument parsing
return goo.ParseArgs[Args]()
}
var Wires = wire.NewSet(
goo.Wires, // includes ProvideShutdownContext, ProvideSlog, ProvideSQLX, ProvideDBMigrator, ProvideMain
ProvideGooConfig, // turn your *Config into *goo.Config
ProvideConfig,
ProvideArgs,
// Provide your own App
wire.Struct(new(App), "*"),
wire.Bind(new(goo.Runner), new(*App)),
)
- Define the injectors in this file.
//go:build wireinject
package cli
import (
"github.com/google/wire"
"github.com/hayeah/goo"
)
func InitMain() (goo.Main, error) {
panic(wire.Build(Wires))
}
After running wire
, a generated file wire_gen.go
will appear with the actual “glue” code.
Create a task in Makefile to run the wire
command.
.PHONY: wire
wire:
go run github.com/google/wire/cmd/wire .
In main.go
, call InitMain()
to get a goo.Main
function, and invoke it:
package main
import (
"github.com/hayeah/goo/examples/cli"
)
func main() {
mainfn, err := cli.InitMain()
if err != nil {
panic(err)
}
mainfn() // calls goo.Main, which will run your App.Run and do graceful shutdown
}
goo.Main
is itself a func()
that orchestrates your application logic (runner.Run()
) and calls shutdown.doExit(...)
if any error occurs.
goo.ShutdownContext
:
- Watches for SIGINT signals.
- Allows you to register cleanup routines (
OnExit(func() error)
) that run before exit. - Offers
BlockExit(func() error)
if you need to do an operation that must finish before the app terminates.
goo.ProvideShutdownContext
will be automatically included if you import goo.Wires
.
As soon as your app calls mainfn()
, ProvideMain(...)
ensures that on any error from your App.Run()
, it calls doExit(...)
gracefully.
func (a *App) Run() error {
// Get the shutdown context from DI
shutdown := a.shutdown
// Register a cleanup function to be executed during shutdown
shutdown.OnExit(func() error {
a.logger.Info("closing database connection")
return a.db.Close()
})
// Register another cleanup function
shutdown.OnExit(func() error {
a.logger.
6972
Info("cleaning up temporary files")
return os.RemoveAll(a.tempDir)
})
// ... rest of your application logic
return nil
}
func (a *App) ProcessImportantData() error {
// This operation must complete before the application terminates
return a.shutdown.BlockExit(func() error {
a.logger.Info("processing important data")
// Simulate a long-running operation
for i := 0; i < 100; i++ {
// Check if we should abort
select {
case <-a.shutdown.Done():
a.logger.Warn("shutdown requested, finishing critical work")
// Complete essential work quickly
return nil
default:
// Continue normal processing
time.Sleep(100 * time.Millisecond)
}
}
a.logger.Info("important data processing completed")
return nil
})
}
If your config includes a DatabaseConfig
(as part of goo.Config
), you can rely on:
goo.ProvideSQLX
– openssqlx.DB
.goo.ProvideDBMigrator
– returns a migrator.App.Run()
can callMigrator.Up(...)
with your table definitions or direct SQL migrations.- sqlx.DB makes it easy to scan into slices and structs. Use that effectively.
[Database]
Dialect = "sqlite3"
DSN = "mydb.sqlite3"
MigrationsPath = "./migrations"
[Logging]
LogLevel = "DEBUG"
LogFormat = "json" # or leave empty for console
[Echo]
Listen = ":8000"