Gopherd/core is a modern Go framework designed for building scalable, modular backend services. It leverages Go's generics to provide a type-safe, flexible, and efficient way to develop applications.
This guide will demonstrate the features and usage of Gopherd/core through a step-by-step web server project. Our example project will include functionalities such as an HTTP server, event system, authentication (auth), and user management.
- Modern, generic-based architecture: Leverage Go's generics for type-safe component creation and management
- Flexible configuration: Load configurations from files, URLs, or standard input with type safety
- Template processing: Use Go templates in your component configurations for dynamic setups
- Multiple format support: Handle JSON, TOML, YAML, and other arbitrary configuration formats through encoders and decoders
- Automatic dependency injection: Simplify component integration with built-in dependency resolution and injection
Ensure you have Go version 1.21 or later. Then, install Gopherd/core in your project:
go get github.com/gopherd/coreOur example project structure is as follows:
example/
├── components
│ ├── auth
│ │ ├── auth.go
│ │ └── authapi
│ │ └── authapi.go
│ ├── blockexit
│ │ └── blockexit.go
│ ├── eventsystem
│ │ └── eventsystem.go
│ ├── httpserver
│ │ ├── httpserver.go
│ │ └── httpserverapi
│ │ └── httpserverapi.go
│ ├── logger
│ │ └── logger.go
│ └── users
│ └── users.go
├── config.json
└── main.go
Let's start by creating a minimal main program. In main.go:
package main
import (
"github.com/gopherd/core/service"
)
funcmain() {
service.Run()
}This simple main program calls service.Run() to start the application. At this point, it won't do anything because we haven't registered any components.
Let's first implement a simple blockexit component, which keeps the program running and allows it to be closed using Ctrl-C.
Create components/blockexit/blockexit.go:
package blockexit
import (
"context""fmt""os""os/signal""github.com/gopherd/core/component"
)
constname="github.com/gopherd/example/components/blockexit"funcinit() {
component.Register(name, func() component.Component {
returnnew(blockExitComponent)
})
}
typeblockExitComponentstruct {
component.BaseComponent[struct{}]
}
func (b*blockExitComponent) Start(ctx context.Context) error {
fmt.Println("Starting blockExitComponent")
c:=make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-cfmt.Println("Received interrupt signal")
returnnil
}Then modify main.go:
package main
import (
"github.com/gopherd/core/service"// Import the component, the init method of the component package will register the component
_ "github.com/gopherd/example/components/blockexit"
)
funcmain() {
service.Run()
}The main function hasn't changed; it will always remain this way. We modified it by adding an import to include the implemented component. Now you can execute it in the command line:
echo'{"Components":[{"Name":"github.com/gopherd/example/components/blockexit"}]}'| go run main.go -After running, you'll see the output Starting blockExitComponent, and the program won't exit. Use Ctrl-C to exit.
Note: Pay attention to the following:
- It's recommended to use the package name as the component name to avoid accidental name duplication.
- This run doesn't use a configuration file but reads configuration information through standard input, which only configures one blockexit component.
- We also support obtaining configurations from files and HTTP services. Use
-hto view the usage help.
The previous component was too simple. Let's start by implementing a basic HTTP server component to understand the basic structure and registration process of components.
First, let's create the API definition for the HTTP server component. In components/httpserver/httpserverapi/httpserverapi.go:
package httpserverapi
import"net/http"typeComponentinterface {
HandleFunc(patternstring, handler http.HandlerFunc)
}Then, we implement the HTTP server component. In components/httpserver/httpserver.go:
package httpserver
import (
"context""fmt""net/http""github.com/gopherd/core/component""github.com/gopherd/example/components/httpserver/httpserverapi"
)
constname="github.com/gopherd/example/components/httpserver"funcinit() {
component.Register(name, func() component.Component {
returnnew(httpserverComponent)
})
}
// Assert that httpserverComponent implements the httpserverapi.Component interfacevar_ httpserverapi.Component= (*httpserverComponent)(nil)
typehttpserverComponentstruct {
component.BaseComponent[struct {
Addrstring
}]
server*http.Server
}
func (h*httpserverComponent) Init(ctx context.Context) error {
addr:=h.Options().Addrifaddr=="" {
addr=":http"
}
h.server=&http.Server{Addr: addr}
returnnil
}
func (h*httpserverComponent) Start(ctx context.Context) error {
fmt.Println("Starting HTTP server", "addr", h.server.Addr)
gofunc() {
iferr:=h.server.ListenAndServe(); err!=nil&&err!=http.ErrServerClosed {
h.Logger().Error("HTTP server error", "error", err)
}
}()
returnnil
}
func (h*httpserverComponent) Shutdown(ctx context.Context) error {
fmt.Println("Shutting down HTTP server")
returnh.server.Shutdown(ctx)
}
func (h*httpserverComponent) HandleFunc(patternstring, handler http.HandlerFunc) {
http.HandleFunc(pattern, handler)
}Create a basic config.json file:
{
"Components": [
{
"Name": "github.com/gopherd/example/components/httpserver",
"UUID": "httpserver",
"Options": {
"Addr": ":8080"
}
},
{
"Name": "github.com/gopherd/example/components/blockexit"
}
]
}Now, let's update main.go to import the HTTP server component:
package main
import (
"github.com/gopherd/core/service"// Import components, the init method of the component package will register the components
_ "github.com/gopherd/example/components/blockexit"
_ "github.com/gopherd/example/components/httpserver"
)
funcmain() {
service.Run()
}Now you can run your application:
go run main.go config.jsonThis will start a basic HTTP server listening on port 8080. After starting, you'll see output like this:
Starting HTTP server addr :8080
Starting blockExitComponent
After pressing Ctrl-C, the program will close and output:
Received interrupt signal
Shutting down HTTP server
Here we can basically explain:
Component Development: Embed a component.BaseComponent[T], where the generic parameter T is the component's configuration, accessed through the component's Options() in the code. Then you can choose to implement the component's lifecycle functions such as Init, Start, Shutdown (and Uninit which hasn't appeared here).
Component Registration: In the init function, component.Register is called to register a constructor function that creates our implemented component object based on the component's name. This function doesn't need to initialize any data for the component; for any component, just using new to create is sufficient. Finally, importing this package in main completes the registration.
Component Configuration: Registered components won't run by themselves; they need to be added to the Components array in the configuration file.
Component Order: The order in Components is the calling order of Init and Start functions, while Shutdown and Uninit are in reverse order. Based on our current configuration, the lifecycle function execution order of the two components is as follows:
blockexit.Init -> httpserver.Init ->
blockexit.Start -> httpserver.Start ->
httpserver.Shutdown -> blockexit.Shutdown ->
httpserver.Uninit -> blockexit.Uninit -> Program exits
Gopherd/core provides a flexible configuration management mechanism, including template features. Let's delve into how to use these functionalities.
The configuration file is typically a JSON file containing the following main sections:
Context: Global context, which can be used in templatesComponents: List of components, each containing a requiredNameand optionalUUID,Refs,Options
The configuration file supports Go's template syntax. You can use {{.}} to reference values in the context. For example:
{
"Context": {
"Namespace": "example",
"Name": "example-server",
"ID": 1001,
"R": {
"HTTPServer": "httpserver"
}
},
"Components": [
{
"Name": "github.com/gopherd/example/components/httpserver",
"UUID": "{{.R.HTTPServer}}",
"Options": {
"Addr": ":{{add 8000 .ID}}"
}
},
{
"Name": "github.com/gopherd/example/components/blockexit"
}
]
}In this example:
{{.R.HTTPServer}}will be replaced with "httpserver"{{add 8000 .ID}}will be calculated as 9001 (8000 + 1001)
To use templates, you need to add the -T parameter when running the program, indicating that templates are enabled. By default, they are not enabled.
For configurations using the JSON format, we've taken into account that you might need to include some explanatory notes. Therefore, we support line comments that begin with // at the start of a line (allowing for leading whitespace). However, block comments using /* ... */ are not supported.
This approach allows you to add comments to your JSON configuration files for better readability and maintenance, while still maintaining compatibility with standard JSON parsers when the comments are stripped out.
Examples of valid comments:
Examples of invalid comments:
{
/* Invalid comment */"Context": { // Invalid comment"ID": 1001, // Also an invalid comment
},
"Components": [
/*Invalid comment*/
]
}First, you need to modify the main function.
For the TOML format:
funcmain() {
service.Run(service.WithEncoder(toml.Marshal), service.WithDecoder(toml.Unmarshal))
}For the YAML format:
funcmain() {
service.Run(service.WithEncoder(yaml.Marshal), service.WithDecoder(yaml.Unmarshal))
}You can choose from several popular libraries for the toml and yaml packages. For example, the following libraries are widely supported:
Note: It's important to mention that the current support for toml and yaml is implemented through a conversion process. The configuration file is first parsed into a map[string]any using the provided Decoder, then encoded into JSON. The rest of the program will continue to use json thereafter.
By passing encoder and decoder parameters as demonstrated above, you can support configuration in any arbitrary format.
Now, let's implement other core components, including EventSystem, Auth, and Users components.
First, let's implement the event system component.
package eventsystem
import (
"context""reflect""github.com/gopherd/core/component""github.com/gopherd/core/event"
)
constname="github.com/gopherd/example/components/eventsystem"funcinit() {
component.Register(name, func() component.Component {
returnnew(eventsystemComponent)
})
}
// We didn't define a separate eventsystemapi package, directly using event.EventSystem as the component's exported interfacevar_ event.EventSystem[reflect.Type] = (*eventsystemComponent)(nil)
typeeventsystemComponentstruct {
component.BaseComponent[struct {
Ordered*bool
}]
event.EventSystem[reflect.Type]
}
func (com*eventsystemComponent) Init(ctx context.Context) error {
ordered:=trueifcom.Options().Ordered!=nil {
ordered=*com.Options().Ordered
}
com.EventSystem= event.NewEventSystem[reflect.Type](ordered)
returnnil
}The Auth component handles user authentication. In components/auth/auth.go:
package auth
import (
"context""fmt""net/http""reflect""github.com/gopherd/core/component""github.com/gopherd/core/event""github.com/gopherd/example/components/auth/authapi""github.com/gopherd/example/components/httpserver/httpserverapi"
)
constname="github.com/gopherd/example/components/auth"var_ authapi.Component= (*authComponent)(nil)
funcinit() {
component.Register(name, func() component.Component {
returnnew(authComponent)
})
}
typeauthComponentstruct {
component.BaseComponentWithRefs[struct {
Secretstring
}, struct {
HTTPServer component.Reference[httpserverapi.Component]
EventSystem component.Reference[event.EventSystem[reflect.Type]]
}]
}
func (a*authComponent) Start(ctx context.Context) error {
fmt.Println("Starting Auth component")
a.Refs().HTTPServer.Component().HandleFunc("/login", a.handleLogin)
returnnil
}
func (a*authComponent) handleLogin(w http.ResponseWriter, r*http.Request) {
username:=r.FormValue("username")
// Simple authentication logic, should be more secure in actual applicationsifusername!="" {
a.Refs().EventSystem.Component().DispatchEvent(context.Background(), &authapi.LoginEvent{Username: username})
w.Write([]byte("Login successful"))
} else {
http.Error(w, "Invalid username", http.StatusBadRequest)
}
}In components/auth/authapi/authapi.go:
package authapi
import (
"context""reflect""github.com/gopherd/core/event"
)
typeComponentinterface {
// Define public methods for the Auth component here, if any// If there are none, this interface can be omitted
}
// Events can also be defined here, or events can be centrally defined in the projecttypeLoginEventstruct {
Usernamestring
}
// The following code about event types and Listeners can be generated using the github.com/gopherd/tools/cmd/eventer tool, via go generatevarloginEventType=reflect.TypeOf((*LoginEvent)(nil))
func (e*LoginEvent) Typeof() reflect.Type {
returnloginEventType
}
funcinit() {
event.Register(new(LoginEvent))
}
funcLoginEventListener(ffunc(context.Context, *LoginEvent) error) event.Listener[reflect.Type] {
returnevent.Listen(loginEventType, f)
}The Users component handles user-related functionality. In components/users/users.go:
package users
import (
"context""fmt""net/http""reflect""github.com/gopherd/core/component""github.com/gopherd/core/event""github.com/gopherd/example/components/auth/authapi""github.com/gopherd/example/components/httpserver/httpserverapi"
)
constname="github.com/gopherd/example/components/users"funcinit() {
component.Register(name, func() component.Component {
returnnew(usersComponent)
})
}
typeusersComponentstruct {
component.BaseComponentWithRefs[struct {
MaxUsersint
}, struct {
HTTPServer component.Reference[httpserverapi.Component]
EventSystem component.Reference[event.EventSystem[reflect.Type]]
}]
loggedInUsersmap[string]bool
}
func (u*usersComponent) Init(ctx context.Context) error {
u.loggedInUsers=make(map[string]bool)
returnnil
}
func (u*usersComponent) Start(ctx context.Context) error {
fmt.Println("Starting Users component")
u.Refs().HTTPServer.Component().HandleFunc("/profile", u.handleProfile)
u.Refs().EventSystem.Component().AddListener(authapi.LoginEventListener(u.onLoginEvent))
returnnil
}
func (u*usersComponent) handleProfile(w http.ResponseWriter, r*http.Request) {
username:=r.URL.Query().Get("username")
ifusername=="" {
http.Error(w, "Username is required", http.StatusBadRequest)
return
}
ifu.loggedInUsers[username] {
fmt.Fprintf(w, "Profile for user: %s", username)
} else {
http.Error(w, "User not logged in", http.StatusUnauthorized)
}
}
func (u*usersComponent) onLoginEvent(ctx context.Context, e*authapi.LoginEvent) error {
fmt.Println("User logged in", "username", e.Username)
u.loggedInUsers[e.Username] =trueiflen(u.loggedInUsers) >u.Options().MaxUsers {
fmt.Println("Warning: Too many users logged")
}
returnnil
}Now that we have implemented the core components, let's continue to refine our application.
In our example, both the Auth and Users components depend on the HTTPServer and EventSystem components. These dependencies are established through the Refs field and the UUID in the configuration file.
For example, in the Auth component:
typeauthComponentstruct {
component.BaseComponentWithRefs[struct{
Secretstring
}, struct{
HTTPServer component.Reference[httpserverapi.Component]
EventSystem component.Reference[event.EventSystem[reflect.Type]]
}]
}Here, the HTTPServer and EventSystem fields define the Auth component's dependencies on these two components.
In the configuration file, we specify these dependencies through the Refs field, where the value is the UUID of the corresponding component:
{
"Name": "auth",
"Refs": {
"HTTPServer": "{{.R.HTTPServer}}",
"EventSystem": "{{.R.EventSystem}}"
},
"Options": {
"Secret": "{{.Namespace}}-secret"
}
}Note: Some might ask, why not use the component name as the basis for dependencies, and instead need a UUID? This is because some components may exist multiple times in a service, such as DB, connecting to different databases resulting in multiple DB component instances. When referencing, it's not possible to distinguish based on the component name.
API packages (such as httpserverapi, authapi) play a crucial role in our architecture. They define the interfaces exposed by each component, allowing other components to depend on these interfaces rather than specific implementations.
This design has several benefits:
- Decoupling: Components interact through interfaces rather than specific implementations, reducing coupling.
- Flexibility: It's easy to replace component implementations as long as the new implementation satisfies the interface definition.
- Avoiding Circular Dependencies: By separating interface definitions and implementations, package-level circular dependencies can be effectively avoided.
For example, the httpserverapi.Component interface defines the functionality that the HTTP server component should provide, without involving specific implementation details:
typeComponentinterface {
HandleFunc(patternstring, handler http.HandlerFunc)
}Other components (such as Auth and Users) can depend on this interface without needing to know the specific implementation details of the HTTP server.
Let's expand our configuration file to add the newly developed components:
{
"Context": {
"Namespace": "example",
"Name": "example-server",
"ID": 1001,
"R": {
"HTTPServer": "httpserver",
"EventSystem": "eventsystem"
}
},
"Components": [
{
"Name": "github.com/gopherd/example/components/eventsystem",
"UUID": "{{.R.EventSystem}}",
"Options": {
"Ordered": true
}
},
{
"Name": "github.com/gopherd/example/components/auth",
"Refs": {
"HTTPServer": "{{.R.HTTPServer}}",
"EventSystem": "{{.R.EventSystem}}"
},
"Options": {
"Secret": "{{.Namespace}}-secret"
}
},
{
"Name": "github.com/gopherd/example/components/users",
"Refs": {
"HTTPServer": "{{.R.HTTPServer}}",
"EventSystem": "{{.R.EventSystem}}"
},
"Options": {
"MaxUsers": 1000
}
},
{
"Name": "github.com/gopherd/example/components/httpserver",
"UUID": "{{.R.HTTPServer}}",
"Options": {
"Addr": ":{{add 8000 .ID}}"
}
},
{
"Name": "github.com/gopherd/example/components/blockexit"
}
]
}Then update main.go:
package main
import (
"github.com/gopherd/core/service"// Import components, the init method of the component package will register components
_ "github.com/gopherd/example/components/auth"
_ "github.com/gopherd/example/components/blockexit"
_ "github.com/gopherd/example/components/eventsystem"
_ "github.com/gopherd/example/components/httpserver"
_ "github.com/gopherd/example/components/users"
)
funcmain() {
service.Run()
}Now we can run it, noting that we add the -T parameter when starting to use templates.
go run main.go -T config.jsonAt this point, you should see the following output:
Starting Auth component
Starting Users component
Starting HTTP server addr :9001
Starting blockExitComponent
Let's try logging in by visiting http://localhost:9001/login?username=xiaowang in a browser, or using the curl command:
curl http://localhost:9001/login?username=xiaowangIf everything is normal, the access will receive a Login successful response, and the control output will show:
User logged in username xiaowang
This is the output of the LoginEvent event that the users component is listening to. Now, everything is functioning normally. What we have developed is a series of components like this, implementing mutual calls through dependency injection, and also communicating through the event system. Next, let's develop a logger component to handle logging.
Gopherd/core uses the log/slog package from the Go standard library for logging. Each component can use its Logger() method to obtain a Logger with component context to output logs, which is the recommended approach as it tracks which component the log originates from.
Example of using logs in a component:
func (c*myComponent) doSomething() {
c.Logger().Info("Doing something", "key", "value")
// orc.Logger().Info("Doing something", slog.String("key", "value"))
}Gopherd/core only uses the log/slog logging tool. For information about log/slog, you can refer to this article https://betterstack.com/community/guides/logging/logging-in-go/ for an introduction and explanation, as well as the official documentation. In short, after the introduction of log/slog, it is no longer recommended to develop your own or use other open-source logging systems. Any other custom handling of logs can be done by implementing slog.Handler. The Logger() method of components is to obtain a *slog.Logger containing basic context information of the component.
Up to now, we haven't output any logs, but have been using fmt.Println. Next, we will package the initialization of logs into a component and configure it in Components.
Let's add components/logger/logger.go:
package logger
import (
"context""errors""io""log/slog""os""github.com/gopherd/core/component"
)
constname="github.com/gopherd/example/components/logger"funcinit() {
component.Register(name, func() component.Component {
returnnew(loggerComponent)
})
}
typeloggerComponentstruct {
component.BaseComponent[struct {
JSONbool// Whether to use JSON format outputLevel slog.Level// Log levelOutputstring// Where to output logs, here we simply implemented stderr, stdout, discard
}]
}
func (com*loggerComponent) Init(ctx context.Context) error {
output, err:=com.createOutput()
iferr!=nil {
returnerr
}
opts:=&slog.HandlerOptions{
Level: com.Options().Level,
}
varhandler slog.Handlerifcom.Options().JSON {
handler=slog.NewJSONHandler(output, opts)
} else {
handler=slog.NewTextHandler(output, opts)
}
slog.SetDefault(slog.New(handler))
returnnil
}
func (com*loggerComponent) createOutput() (io.Writer, error) {
switchcom.Options().Output {
case"stderr":
returnos.Stderr, nilcase"stdout":
returnos.Stdout, nilcase"":
returnio.Discard, nildefault:
returnnil, errors.New("unsupported output")
}
}Then update main.go:
package main
import (
"github.com/gopherd/core/service"// Import components, the init method of the component package will register components
_ "github.com/gopherd/example/components/auth"
_ "github.com/gopherd/example/components/blockexit"
_ "github.com/gopherd/example/components/eventsystem"
_ "github.com/gopherd/example/components/httpserver"
_ "github.com/gopherd/example/components/logger"
_ "github.com/gopherd/example/components/users"
)
funcmain() {
service.Run()
}In the config.json configuration file, also add logger. Considering that everyone should use logs, the logger component should be initialized first, so logger should be the first component (when configuring components, sometimes you need to consider which components go first and which go last, for example, blockexit is always the last one, and httpserver is placed second to last because we want other components to be ready before starting the HTTP interface for client access). The latest config.json is as follows:
{
"Context": {
"Namespace": "example",
"Name": "example-server",
"ID": 1001,
"R": {
"HTTPServer": "httpserver",
"EventSystem": "eventsystem"
}
},
"Components": [
{
"Name": "github.com/gopherd/example/components/logger",
"Options": {
"Level": "DEBUG",
"Output": "stdout"
}
},
{
"Name": "github.com/gopherd/example/components/eventsystem",
"UUID": "{{.R.EventSystem}}",
"Options": {
"Ordered": true
}
},
{
"Name": "github.com/gopherd/example/components/auth",
"Refs": {
"HTTPServer": "{{.R.HTTPServer}}",
"EventSystem": "{{.R.EventSystem}}"
},
"Options": {
"Secret": "{{.Namespace}}-secret"
}
},
{
"Name": "github.com/gopherd/example/components/users",
"Refs": {
"HTTPServer": "{{.R.HTTPServer}}",
"EventSystem": "{{.R.EventSystem}}"
},
"Options": {
"MaxUsers": 1000
}
},
{
"Name": "github.com/gopherd/example/components/httpserver",
"UUID": "{{.R.HTTPServer}}",
"Options": {
"Addr": ":{{add 8000 .ID}}"
}
},
{
"Name": "github.com/gopherd/example/components/blockexit"
}
]
}Change all the places where fmt.Println was used for output to the component's Logger().Info function. For example, the Start function of the httpserver component:
func (h*httpserverComponent) Start(ctx context.Context) error {
h.Logger().Info("Starting HTTP server", "addr", h.server.Addr)
gofunc() {
iferr:=h.server.ListenAndServe(); err!=http.ErrServerClosed {
h.Logger().Error("HTTP server error", "error", err)
}
}()
returnnil
}Replace others with logs as well, then run the program:
go run main.go -T config.jsonThe output should look something like this, quite verbose:
time=2024-08-11T01:10:34.156+08:00 level=INFO msg="component initialized" component=github.com/gopherd/example/components/logger
time=2024-08-11T01:10:34.157+08:00 level=INFO msg="initializing component" component=github.com/gopherd/example/components/eventsystem#eventsystem
time=2024-08-11T01:10:34.157+08:00 level=INFO msg="component initialized" component=github.com/gopherd/example/components/eventsystem#eventsystem
....... other omitted .......
In fact, the framework logs before and after calling all 4 lifecycle functions of the components, so usually when implementing components, there's no need to log again at the beginning of the lifecycle.
Alright, by now, we have implemented the basic structure of our app. For more functionality, we just need to develop components and configure them. Each component completes its own work. Other things we use, including DB and redis, should be wrapped as components for use. Anything can be provided as a component for use. But sometimes there are some auxiliary functions that are still provided as corresponding packages. Components are for managing resources and functionalities with lifecycles.
Finally, let's take a look at the current directory structure:
example/
├── components
│ ├── auth
│ │ ├── auth.go
│ │ └── authapi
│ │ └── authapi.go
│ ├── blockexit
│ │ └── blockexit.go
│ ├── eventsystem
│ │ └── eventsystem.go
│ ├── httpserver
│ │ ├── httpserver.go
│ │ └── httpserverapi
│ │ └── httpserverapi.go
│ ├── logger
│ │ └── logger.go
│ └── users
│ └── users.go
├── config.json
└── main.go
The code for this example project is hosted at https://github.com/gopherd/example.
The component dependency mechanism in Gopherd/core is based on several key concepts:
- UUID: Each component that can be depended upon has a unique identifier (UUID).
- Refs: Components declare their dependencies on other components through the Refs field.
- Configuration File: In the configuration file, the UUID of the dependent component is associated with the components that depend on it through the Refs field.
The framework automatically parses these dependency relationships during initialization and injects the correct component instances into the components that depend on them.
When designing API packages, the following principles should be followed:
- Only define interfaces, do not include implementation details.
- Interfaces should be small and focused, only including necessary methods.
- Use generic types, avoid introducing types specific to a particular implementation.
- Consider future extensibility, but don't over-design.
To avoid circular dependencies, the following strategies can be adopted:
- Use dependency injection: Inject dependencies through the Refs field instead of direct imports.
- Separate interfaces and implementations: Put interface definitions in separate API packages.
- Redesign: If circular dependencies occur, it may indicate a need to reconsider the division of component responsibilities.
Gopherd/core provides several useful command line arguments:
-p: Print parsed configuration and exit-t: Test configuration validity and exit-T: Enable configuration template processing
Usage examples:
# Enable template processing and run the application
go run main.go -T config.json
# Print parsed configuration. Without -T, it outputs the original config.json, while with -T, it outputs the configuration after template processing
go run main.go -p config.json
go run main.go -p -T config.json
# Test configuration validity
go run main.go -t config.json
go run main.go -t -T config.jsonThe component design of Gopherd/core makes testing simple.
- Write unit tests for each component
- Use mock objects to simulate dependent components
- Write integration tests to test interactions between components
- Allocate resources in the Init method, release resources in the Uninit method.
- Use defer statements to ensure resources are properly released.
- Gracefully close long-running operations in the Shutdown method.
- The Init method should only be responsible for initializing itself. At this point, it should not use dependent components as they may not have completed initialization. If you need to use dependent components for initialization, that part of initialization should be placed in Start. Remember this!
The Gopherd/core framework provides a powerful and flexible way to build modular Go applications. By using the component system, dependency injection, and event mechanism, highly maintainable and extensible applications can be created.
This guide covers the core concepts and usage methods of Gopherd/core, from basic component implementation to advanced features and best practices. By following these guidelines, developers can fully leverage the advantages of the Gopherd/core framework to build robust, efficient backend services.
As you delve deeper into using the framework, you'll discover more possibilities and uses. Continuous practice and exploration will help you better master Gopherd/core and maximize its potential in real projects.
{ // Valid comment// Still a valid comment"Context": { // Also a valid comment"ID": 1001 }, "Components": [ // Still a valid comment ] }