- Each component should reside in its own package.
- Use descriptive package names that reflect the component's functionality (e.g.,
tcpserver,logger,redis).
- If the component has a public API, define it in a separate
apior[component]apipackage (e.g.,redisapi,dbapi). - Keep the API package lightweight, containing only interface definitions and essential types.
- Main component logic goes in a file named after the package (e.g.,
redis.goin theredispackage). - Use additional files for specific functionalities if needed (e.g.,
pid_unix.go,pid_windows.goin thepidfilepackage).
- Define a
Nameconstant for each component:const Name = "your/repository/path/components/[component-name]"
- Use a lowercase name ending with "Component" (e.g.,
redisComponent,loggerComponent). - Keep the struct unexported to encapsulate internal details.
- Name it
Optionsand keep it exported for configuration.
- Name interfaces ending with
ComponentorAPIin the API package (e.g.,Componentinredisapi).
- Use an
init()function to register the component:func init() { component.Register(Name, func() component.Component { return &componentNameComponent{} }) }
- Important: The registration function should only create and return the component object. Do not initialize any fields here. All initialization should be done in the
Initmethod.
- Define an
Optionsstruct with all configurable parameters. - Use meaningful field names and add comments for clarity.
- Use
time.Durationfor time-related configurations. - For boolean flags, define them so that the default (zero) value is false and represents the most common or safest configuration.
- If a flag needs to be true by default, invert its meaning in the name (e.g., use
DisableFeatureinstead ofEnableFeature).
- Implement
OnLoadedmethod to set default values or validator for options:func (o *Options) OnLoaded() error { // Set default values // Validate values }
- Embed
component.BaseComponent[Options]for basic components. - Embed
component.BaseComponentWithRefs[Options, Refs]for components with references to other components.
- Ensure the component implements necessary interfaces.
- Use a compile-time check to ensure interface compliance:
var _ componentapi.Component = (*componentNameComponent)(nil)
- Implement
Init,Start,Shutdown, andUninitmethods as needed. - Use context for cancellation support in these methods.
- Lifecycle method pairs:
InitandUninit: IfInitis called,Uninitwill always be called. IfInitis not called,Uninitwill not be called.StartandShutdown: Similarly, ifStartis called,Shutdownwill always be called. IfStartis not called,Shutdownwill not be called.
- In
Init:- Set default options
- Initialize the component's own resources and fields
- Do not access or use referenced components in
Init
- After
Init, ensure that the component's public API is ready for use by other components - In
Start:- Begin main component operations
- Start background goroutines if needed
- You can now safely use referenced components
- In
Shutdown:- Stop all operations gracefully
- Release resources
- Wait for goroutines to finish
- In
Uninit:- Perform final cleanup
- Release any resources that weren't handled in
Shutdown - This method is called after
Shutdownand should handle any remaining cleanup tasks
- Return errors from methods instead of panicking.
- Use descriptive error messages, wrapping errors for context when appropriate.
- Create custom error types if needed.
- Use the logger provided by the base component (
c.Logger()). - Log important events, errors, and state changes.
- Use appropriate log levels (Info, Warn, Error).
- Use goroutines for concurrent operations.
- Implement proper shutdown mechanisms to stop goroutines gracefully.
- Use
sync.Mutexorsync.RWMutexfor protecting shared resources. - Consider using
atomicoperations for simple counters.
- Use channels for communication between goroutines.
- Use
selectstatements with context cancellation for graceful shutdowns.
- Always close opened resources (files, network connections, etc.).
- Use
deferfor cleanup operations where appropriate.
- Define clear, minimal interfaces in the API package.
- Focus on the core functionality of the component.
- Use context as the first parameter for methods that may be long-running.
- Return errors as the last return value.
- Use functional options pattern for complex configurations when appropriate.
- Use build tags for platform-specific implementations (e.g.,
abc_unix.go,abc_windows.go). - Provide fallback implementations for unsupported platforms when possible.
- Write unit tests for component logic.
- Use table-driven tests for multiple test cases.
- Mock external dependencies for isolated testing.
- Provide clear godoc comments for exported types, functions, and methods.
- Include usage examples in package documentation.
- Use
component.Referencefor required component references.- After
Init, these referenced components are guaranteed to be available and their APIs can be used directly.
- After
- Use
component.OptionalReferencefor optional component references.- Even after
Init, always check ifComponent()is nil before using optional references. - Optional components may not be injected, so your component should handle their absence gracefully.
- Even after
Remember that the framework automatically handles dependency injection, so you don't need to manually resolve references in the Init method.
By following these practices, you'll create components that are consistent, maintainable, and work well within the framework's design. Always ensure that your component's public API is ready for use after Init, and only interact with other components from the Start method onwards.