A small GO library, which may come handy for recurring tasks or simple use cases.
A short briefing of the single modules:
The cli manager can help to get parameters, which are passed as cli arguments and also are provided as environment variables.
A simple example:
func LoadCliParameters() {
file := cli.NewParameter[string]("file", "file.yaml", "description", "ENV_VAR_FILE")
dryRun := cli.NewParameter[bool]("dryRun", false, "description", "ENV_VAR_DRY_RUN")
mgr := cli.New()
mgr.AddStringParameter(file)
mgr.AddBoolParameter(dryRun)
mgr.Parse()
fmt.Println(*file.GetValue())
fmt.Println(*dryRun.GetValue())
}The command package provides a way to execute external commands with logging and dry-run support. Parameters can be masked so that sensitive values (e.g. passwords) are hidden in log output.
WARNING: This package passes commands directly to
exec.Commandwithout sanitization. The caller must ensure all inputs are trusted. Do not pass unsanitized user input.
A simple example:
logger := logging.NewLogger()
// Direct usage
cmd := command.NewCommand(logger, false)
params := command.NewParameters(
command.WithCommand("echo"),
command.WithValue("hello"),
command.WithParam("--verbose"),
command.WithValueMasked("s3cret"), // will be shown as *** in logs
)
err := cmd.Execute(params)
// Or via the Manager (builder pattern)
mgr := command.NewCommandManager(logger, true) // true = dry-run
mgr.AddParameter(command.WithCommand("echo"))
mgr.AddParameter(command.WithValue("hello"))
err = mgr.ExecuteCommand()Loads configuration into a struct. Config can come from a file or the os environment or both. The "env" tag must be used to configure the environment loading.
Example:
type ConfigData struct {
Data1 string `env:"name=DATA_ONE_STR"`
Data2 string `env:""`
Data3 string `env:"self"`
Data4 int `env:"name=DATA_ONE_INT"`
Data5 map[string]string `env:"prefix=GMD,cutoff=false"`
Data6 map[string]float32 `env:"prefix=DAT,cutoff=true"`
Data7 map[string]string `env:"prefix=CUT"`
}There are two main categories:
- single (named) values -> string, int, bool, ...
- multiple named values -> map[string]string, map[string]int, map[string]bool, ...
Tag format:
env:"name=DATA_ONE_STR,default=default string"env:"name=DATA_ONE_STR"env:"self,default=default string"
Explanation:
- "name": defines the name of the environment variable to be loaded into this field
- "self": if "name" is not provided or the "env" tag is empty, the snake_cased field name is used
- "default": defines a default value, if the environment variable is not found
Tag format:
env:"prefix=GMD,cutoff=false"env:"prefix=DAT,cutoff=true"env:"prefix=CUT"
Explanation:
- "prefix": defines the prefix of the environment variables, which are going to be grouped. NOTE: There is always an "_" at the end of the prefix. If it's missing here, it will be attached automatically.
- "cutoff": defines, if the prefix itself will be cut off. Ex.: if true, "PRE_DATA_1" => "DATA_1". If just "cutoff" is provided, it's treated as "cutoff=true" internally.
The tags "name", "self" and "prefix" are ordered:
- name
- self
- prefix
So, if "name" is found, everything else (self, prefix) is ignored.
NOTE: "default" is only considered when "name" or "self" is provided. "cutoff" is only considered when "prefix" is provided.
This provides a simple way to fetch environment variables with defined default values, if they can't be found. There are functions for the following types:
GetEnv– raw stringGetEnvInt– intGetEnvFloat32– float32GetEnvFloat64– float64GetEnvBool– boolGetEnvMap– all variables with a given prefix asmap[string]string
This provides one function at the moment:
- Check, if a file exists and if it is really a file (not a directory)
This is a generic helper package to filter a slice. You provide one or more filter functions to set the matching boundaries.
items := []*MyStruct{ /* ... */ }
f := filter.NewFilter(items)
result := f.Filter( // result is []MyStruct
func(val MyStruct) bool { return val.Age > 18 },
func(val MyStruct) bool { return val.Active },
)
for _, item := range result {
fmt.Println(item)
}The heartbeat package is some kind of timer, which executes a given function at an interval.
It supports context.Context for clean cancellation.
It can be configured, if the first execution should start immediately or after the first interval.
Stop() reliably cancels the heartbeat regardless of whether it was started via Run or
RunForever, and is safe to call concurrently.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hb, err := heartbeat.New(5*time.Second, myFunc, heartbeat.WithNoWait())
if err != nil {
log.Fatal(err)
}
// Non-blocking:
hb.Run(ctx)
// Or blocking (until context is cancelled):
// hb.RunForever(ctx)The logging package is a simple logger, where you can configure the appearance of the log entry.
You can also define the io.Writer for it.
Logger is safe for concurrent use – all writes to the underlying io.Writer are serialized internally.
Available options:
WithName– set the logger nameWithSeverity– set the log level (INFO,DEBUG,WARNING,TRACE)WithLogWriter– set a customio.Writer(default:os.Stderr)WithExtend– add an extension string to the log headerWithNameSpacing/WithSeveritySpacing– control column widths
Structs can be logged in JSON or YAML format via InfoStruct.
Error/Fatal include the passed error's text directly in the log line written through
the configured writer (previously it was printed via the global log package and could
end up on a different destination than the configured one).
The mqtt package is a wrapper for the Paho MQTT client.
It simplifies the configuration and usage of publish and subscribe actions.
Client is safe for concurrent use.
The client supports TLS configuration via NewBrokerBuilder and clean shutdown via context.Context:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
brokerOpt, err := mqtt.NewBrokerBuilder().
WithHost("localhost").
WithPort(1883).
WithProtocol(mqtt.MqttTcp).
Build()
client := mqtt.NewClient(mqtt.WithClientId("my-app"), brokerOpt)
client.Connect()
if err := client.Publish(&mqtt.Message{Topic: "topic", Value: "hello"}); err != nil {
log.Println("publish failed:", err)
}
client.LoopForever(ctx) // blocks until context is cancelledHere at the moment only one function is available:
GetExecutableName()– returns the name of the current executable (without path). Returns an error if the name cannot be determined.
The shutdown package executes registered functions when the app is exiting.
It hooks on SIGTERM (code 15) for graceful shutdown and also supports explicit exit via Exit().
shutdown.GetObserver().AddCommand(func() error {
fmt.Println("Cleaning up...")
return nil
})
// For panic recovery:
defer shutdown.ExitOnPanic()Note:
ExitOnPanic()runs all registered shutdown hooks and then terminates the process (os.Exit(1)). It does not "swallow" the panic and let the program continue.
In this package you will find two functions:
PrettyPrintJson– pretty format any struct as JSONPrettyPrintYaml– pretty format any struct as YAML
This package contains a generic TemplateManager. The purpose is to make GO templates (via the GO templating engine)
accessible via name. So called "named templates".
Every template in this manager can be populated with defined options. Currently only with custom template functions.
Manager is safe for concurrent use.
Note: This package uses
text/templatewhich does not perform HTML escaping. Do not use it for HTML output. Usehtml/templatefrom the standard library instead.
Deprecated: Use the
configpackage withformats.ParseYamlConfiginstead.
The yamlconfig package loads YAML config files into a given struct.
A code review pass (see FINDINGS.md for the full list) fixed several panics, data races and goroutine/connection leaks. Most fixes are drop-in and don't require any code changes. The following changes do affect the public API, though:
-
mqtt.Client.Publish/mqtt.Client.Subscribenow return anerror(previously no return value), so that failed publishes/subscriptions are no longer silently swallowed. Calls that ignore the return value keep compiling as-is:// still compiles, error is just ignored client.Publish(msg) // recommended if err := client.Publish(msg); err != nil { log.Println("publish failed:", err) }
This only breaks code that assigns these methods to a
func(*Message)-typed variable, or defines a custom interface requiring the old (return-value-less) signature. -
config/meta.Kindconstants (Name,Prefix,None) are now typed asKindinstead of untypedintconstants. This only affects code that importsconfig/metadirectly (rather than theconfigpackage) and assigns these constants to a plainintvariable – such code now needs an explicit conversion, e.g.int(meta.Name). -
shutdown.ExitOnPanic()now actually terminates the process (os.Exit(1)) after running the registered shutdown hooks, matching what its documentation always said. Previously it silently recovered the panic and let the program continue running afterward. If your code relied on that (undocumented, buggy) "recover and keep going" behavior, replacedefer shutdown.ExitOnPanic()with your ownrecover()handling. -
filter.Filter.Filter(...)now returns[]Tinstead of*[]T(an unnecessary pointer to an already reference-like slice type). Update code that dereferences the result:// before result := f.Filter(...) for _, item := range *result { ... } // after result := f.Filter(...) for _, item := range result { ... }
Since this module is still pre-1.0 (v0.y.z), these changes are shipped as a minor version
bump rather than a new major version – per semver's rules for initial development, the public
API of a 0.y.z release is not guaranteed to be stable.
Import the module:
go get github.com/pmoscode/go-commonIf you want to get a specific version:
go get github.com/pmoscode/go-common@v0.7.0Most packages have tests, so you can see the usage there. But for most packages, even that is not necessary.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
All packages are populated with go docs. So you should take a look here: https://pkg.go.dev/github.com/pmoscode/go-common