8000 GitHub - hayeah/goo
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

hayeah/goo

Repository files navigation

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

How To Write A Goo Project

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 calls InitMain().
  • 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, using goo.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 the Wires 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.

Example: A CLI App With Sqlite

<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>

Example: JSON API Using Fetch

  • 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
}

High-Level Flow

  1. main.go calls InitMain(), gets a goo.Main function, and calls that function.
  2. InitMain() is defined in wire.go using Google’s Wire for dependency injection.
  3. Wires = wire.NewSet(...) that enumerates all providers from the goo library (goo.Wires) plus your own custom providers.
  4. The App struct in app.go implements goo.Runner. Inside App.Run(), you can do any final initialization such as database migrations.

Defining Your Config

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
}

Loading

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 App Struct (Implements goo.Runner)

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.

Using Wire for Dependency Injection

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)),
)

wire.go

  • 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))
}

wire_gen.go

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 .

The main.go

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.

6. Graceful Shutdown

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.

Examples

Registering Cleanup Functions

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
}

Ensuring Operations Complete Before Shutdown

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
    })
}

7. Database Setup & Migrations

If your config includes a DatabaseConfig (as part of goo.Config), you can rely on:

  • goo.ProvideSQLX – opens sqlx.DB.
  • goo.ProvideDBMigrator – returns a migrator.
  • App.Run() can call Migrator.Up(...) with your table definitions or direct SQL migrations.
  • sqlx.DB makes it easy to scan into slices and structs. Use that effectively.

Example Config (cfg.toml)

[Database]
Dialect = "sqlite3"
DSN = "mydb.sqlite3"
MigrationsPath = "./migrations"

[Logging]
LogLevel = "DEBUG"
LogFormat = "json"    # or leave empty for console

[Echo]
Listen = ":8000"

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

0