Source Code Analysis for Go HTTP Framework Hertz
Tags: go, webdev, beginners, tutorial
Published: 2022-10-30 04:16:40

## CloudWeGo-Hertz

Hertz [həːts] is a **high-performance**, **high-usability**, **extensible** HTTP framework for Go. It’s designed to simplify building microservices for developers.

Hertz was inspired by other open-source frameworks like [fasthttp](https://github.com/valyala/fasthttp), [gin](https://github.com/gin-gonic/gin), and [echo](https://github.com/labstack/echo), in combination with unique challenges faced by ByteDance, Hertz has become production ready and powered ByteDance’s internal services over the years.

Nowadays, as Go gain popularity in microservice development, Hertz will be the right choice if you are looking for a **customizable**, **high-performance** framework to support a variety of use case.

This article will explain the structure of the Hertz source code and analyze the startup of Hertz.

Next, you need to open the official [documentation](https://www.cloudwego.io/docs/hertz/getting-started/) for **Hertz** and clone the [repository](https://github.com/cloudwego/hertz) locally, let's get started.

## Architecture

![image-20220901154829735](https://baize-blog-images.oss-cn-shanghai.aliyuncs.com/img/image-20220901154829735.png)

Above is an architectural design diagram from the official **Hertz** documentation. The components in the picture correspond to the package folders within the hertz source code package.

![image-20220901175437136](https://baize-blog-images.oss-cn-shanghai.aliyuncs.com/img/image-20220901175437136.png)

## Getting Started

Following the [instructions](https://www.cloudwego.io/docs/hertz/getting-started/) in the documentation, we could initialize a minimal `hertz` project via the `hertz` cmd tool.

```shell
# Install the command line tool for hertz, used to generate the initial hertz code
go install github.com/cloudwego/hertz/cmd/hz@latest
# Generate code via the hz tool. If the project created is not in the GOPATH/src path, "-module" needs to be declared
hz new -module hertz-study
```

![image-20220901181637689](https://baize-blog-images.oss-cn-shanghai.aliyuncs.com/img/image-20220901181637689.png)

Run the project then you can access the default HTTP service by `/ping`. 

```bash
curl http://127.0.0.1:8888/ping
# response
{"message":"pong"}% 
```

## Source Code Analysis

### server

Look at the main.go function, which is the startup entry for the hertz service. It initializes a default hertz service, does some registration work, and runs the hertz service (HTTP service).

```go
package main

import (
    "github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
    h := server.Default()

    register(h)
    h.Spin()
}
```

Thinking back to the interface about http://127.0.0.1:8888/ping which was declared by `server.Default()`.

Conversely, if you want to specify custom configurations for HTTP service, you need to pass parameters to the `server.Default()`, Or using the `server.New()` method. 

### server.Default()

```go
// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
   h := New(opts...)
   h.Use(recovery.Recovery())

   return h
}

// New creates a hertz instance without any default config.
func New(opts ...config.Option) *Hertz {
	options := config.NewOptions(opts)
	h := &Hertz{
		Engine: route.NewEngine(options),
	}
	return h
}
```

Looking at the Default() method, we find it calls `server.New()` internally, which accepts an Option array of indeterminate length as a parameter.

```go
// Option is the only struct that can be used to set Options.
type Option struct {
	F func(o *Options)
}

// New creates a hertz instance without any default config.
func New(opts ...config.Option) *Hertz {
	options := config.NewOptions(opts)
	h := &Hertz{
		Engine: route.NewEngine(options),
	}
	return h
}
```

Then we go to the `config.NewOptions` method to see how this Option slice will apply our custom content to the Hertz configuration.

```go
func NewOptions(opts []Option) *Options {
    options := &Options{
        KeepAliveTimeout: defaultKeepAliveTimeout,
        ReadTimeout: defaultReadTimeout,
        IdleTimeout: defaultReadTimeout,
        RedirectTrailingSlash: true,
        RedirectFixedPath: false,
        HandleMethodNotAllowed: false,
        UseRawPath: false,
        RemoveExtraSlash: false,
        UnescapePathValues: true,
        DisablePreParseMultipartForm: false,
        Network: defaultNetwork,
        Addr: defaultAddr,
        MaxRequestBodySize: defaultMaxRequestBodySize,
        MaxKeepBodySize: defaultMaxRequestBodySize,
        GetOnly: false,
        DisableKeepalive: false,
        StreamRequestBody: false,
        NoDefaultServerHeader: false,
        ExitWaitTimeout: defaultWaitExitTimeout,
        TLS: nil,
        ReadBufferSize: defaultReadBufferSize,
        ALPN: false,
        H2C: false,
        Tracers: []interface{}{},
        TraceLevel: new(interface{}),
        Registry: registry.NoopRegistry,
    }
    // apply a custom []Option to Options
    options.Apply(opts)
    return options
}

func (o *Options) Apply(opts []Option) {
    for _, op := range opts {
        op.F(o)
    }
}
```

By looking at the `config.NewOptions` source code, it first initializes an Options structure that holds various initialization information for the Hertz service. The Options properties are fixed by default until the `options.Apply(options)` method is called to apply the custom configuration.

It passes a pointer to the Options structure created by default as an argument to the F method of each Option you declared. It assigns values to the Options structure via the F method call, which, because it is a pointer, will naturally apply all the assignments to the same Options.

And how the specific Option's F method is defined can be implemented flexibly, which is one of the reasons why Hertz has good scalability.

```go
// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
    // "Hertz" is the core structure of the framework
    h := New(opts...)
    h.Use(recovery.Recovery())

    return h
}
```

Notice that there is also a `h.Use(recovery.Recovery())` method.

```go
// Recovery returns a middleware that recovers from any panic.
func Recovery(opts ...Option) app.HandlerFunc {
	cfg := newOptions(opts...)

	return func(c context.Context, ctx *app.RequestContext) {
		defer func() {
			if err := recover(); err != nil {
				stack := stack(3)

				cfg.recoveryHandler(c, ctx, err, stack)
			}
		}()
		ctx.Next(c)
	}
}
```

This is a middleware for recovering from panic. By default, it will print the time, content, and stack information of the error and write a 500. Overriding the Config configuration, you can customize the error printing logic.

By the way, if you Initialize a **Hertz** HTTP service by `server.New()`, which won't hold the `Recovery()` method, and you need to inject it yourself.

```go
package main

import (
    "context"

    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/common/hlog"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
    h := server.New()

    h.Use(recovery.Recovery(
        recovery.WithRecoveryHandler(func(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte) {
            hlog.SystemLogger().CtxErrorf(c, "[Recovery] err=%v\nstack=%s", err, stack)
            ctx.AbortWithStatus(consts.StatusInternalServerError)
        })))
    register(h)
    h.Spin()
}
```

### register()

```go
package main

import (
    "github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
    h := server.Default()

    register(h)
    h.Spin()
}
```

After the above analysis, we know that `server.Default()` completes the declaration of the Hertz structure. Let's pay more attention to the content of the `register(h)`.

```go
// register registers all routers.
func register(r *server.Hertz) {

    router.GeneratedRegister(r)

    customizedRegister(r)
}

// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz) {
    //INSERT_POINT: DO NOT DELETE THIS LINE!
}

// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
    r.GET("/ping", handler.Ping)

    // your code ...
}
```

`register(h)` does two types of route registration internally. The annotation of `GeneratedRegister()` indicates that this part of the route is generated by the IDL.

`customizedRegister()` is used to register custom routes which initializes a `/ping` by default, using it in a very similar way to gin.

### Spin()

Finally, let's analyse the third part of the `main()` method, the `h.Spin()`.

```go
// Spin runs the server until catching os.Signal or error returned by h.Run().
func (h *Hertz) Spin() {
    errCh := make(chan error)
    h.initOnRunHooks(errCh)
    go func() {
        // core
        errCh <- h.Run()
    }()

    signalWaiter := waitSignal
    if h.signalWaiter != nil {
        signalWaiter = h.signalWaiter
    }

    if err := signalWaiter(errCh); err != nil {
        hlog.Errorf("HERTZ: Receive close signal: error=%v", err)
        if err := h.Engine.Close(); err != nil {
            hlog.Errorf("HERTZ: Close error=%v", err)
        }
        return
    }

    hlog.Infof("HERTZ: Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)

    ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
    defer cancel()

    if err := h.Shutdown(ctx); err != nil {
        hlog.Errorf("HERTZ: Shutdown error=%v", err)
    }
}
```

After a series of initialization and declaration operations, `Spin()` is responsible for triggering the run of Hertz and handling any errors during the runtime. The most important step is `errCh <- h.Run()`.

```go
func (engine *Engine) Run() (err error) {
    if err = engine.Init(); err != nil {
        return err
    }

    if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
        return errAlreadyRunning
    }
    defer atomic.StoreUint32(&engine.status, statusClosed)

    // trigger hooks if any
    ctx := context.Background()
    for i := range engine.OnRun {
        if err = engine.OnRun[i](ctx); err != nil {
            return err
        }
    }

    return engine.listenAndServe()
}
```

Then you see the `engine.listenAndServe()` method at the end, which is declared in an interface. Looking at its implementation classes, you find that it can be traced back to the standard and netpoll packages.

![image-20220901215952290](https://baize-blog-images.oss-cn-shanghai.aliyuncs.com/img/image-20220901215952290.png)

As an HTTP framework, the most important thing is to provide network communication capabilities. hertz uses the pluggable network library **netpoll** to handle network communication and further optimize performance.

The service is now running, and you can send requests via the console:

```go
curl http://127.0.0.1:8888/ping
{"message":"pong"}% 
```

## Summary

After generating the simplest Hertz code using the hz tool, this article roughly analyses the contents of the main method, dividing it into three parts, the service configuration declaration `Default()`, the route registration `register()` and the HTTP service start `Spin()`.

If you want to know more about Hertz, you could visit the Reference.

## Reference List

- [Hertz official document](https://www.cloudwego.io/docs/hertz/)
- [CloudWeGo/hertz](https://github.com/cloudwego/hertz)
- [CloudWeGo/hertz-benchmark](https://github.com/cloudwego/hertz-benchmark/)
- [CloudWeGo/hertz-contrib](https://github.com/hertz-contrib)