- 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
api
or[component]api
package (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.go
in theredis
package). - Use additional files for specific functionalities if needed (e.g.,
pid_unix.go
,pid_windows.go
in thepidfile
package).
- Define a
Name
constant 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
Options
and keep it exported for configuration.
- Name interfaces ending with
Component
orAPI
in the API package (e.g.,Component
inredisapi
).
- 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
Init
method.
- Define an
Options
struct with all configurable parameters. - Use meaningful field names and add comments for clarity.
- Use
time.Duration
for 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
DisableFeature
instead ofEnableFeature
).
- Implement
OnLoaded
method 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
, andUninit
methods as needed. - Use context for cancellation support in these methods.
- Lifecycle method pairs:
Init
andUninit
: IfInit
is called,Uninit
will always be called. IfInit
is not called,Uninit
will not be called.Start
andShutdown
: Similarly, ifStart
is called,Shutdown
will always be called. IfStart
is not called,Shutdown
will 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
Shutdown
and 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.Mutex
orsync.RWMutex
for protecting shared resources. - Consider using
atomic
operations for simple counters.
- Use channels for communication between goroutines.
- Use
select
statements with context cancellation for graceful shutdowns.
- Always close opened resources (files, network connections, etc.).
- Use
defer
for 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.Reference
for required component references.- After
Init
, these referenced components are guaranteed to be available and their APIs can be used directly.
- After
- Use
component.OptionalReference
for 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.