Skip to content
:tech
← Articles

AI-translated. Read the original in KoreanEmbedded slides, videos and images remain in their original language.

Building a Server in Go

Building a Server in Go

Getting started with Go server development

Reimplement the busiest APIs in another language for better performance

Most of our server ran on Django and could be grouped into ad allocation, content allocation, and the HoneyScreen app service. Python’s characteristics made very high performance difficult to achieve. Across these three services, the allocation APIs—which decide which content or ad to show on a lockscreen—accounted for around 80% of calls. Our team wondered whether reimplementing those APIs in a faster language would help.

Why Go?

I was already familiar with compiled languages such as Java and C#. After considering Java, C, and the relatively new Go, and reading performance references such as the post below, I began porting the APIs to Go. My first impression was that Go was less verbose than Java or C-family languages, while offering stronger typing and encapsulation than Python. That suited my preference for limiting flexibility where it makes code easier to read.

Source: Carles Mateo, Performance of several languages

Server development environment

Server design

Importing libraries

  • GVT (https://github.com/FiloSottile/gvt) — Go dependencies can be managed with a vendoring tool. GVT wasn’t particularly well known when we adopted it, but its simplicity appealed to us. It tracks the revisions in use as shown below, and its update command fetches newer source versions.
{
  "version": 0,
  "dependencies": [
    {
      "importpath": "github.com/Buzzvil/go-env",
      "repository": "https://github.com/Buzzvil/go-env",
      "vcs": "git",
      "revision": "2d8489d40184a12c4d09d09ce1ff717e5dbb0745",
      "branch": "master",
      "notests": true
    },
....

Design pattern

Go disallows cyclic dependencies at the package level, which helped us create a clearer structure. For example, we enforced that Services could not reference Controllers, and Models could not reference Controllers, Services, or DTOs. Every API request passes through a Route to a Controller. The Controller handles the resulting Data Transfer Objects either directly or through the Service layer. Database access happens through Models or directly; if the structure becomes more complex, we plan to introduce Data Access Objects to handle queries and related work.

Libraries

ComponentLibraryWhy we chose it
NetworkGinNetwork performance was our top priority for a web server. We chose Gin after reviewing its benchmarks.
Redis & cachego-redisPerformance was again the main criterion.
MySQLGormAn ORM is hard to do without. Gorm supports multiple databases and offers method chaining.
Dynamoguregu dynamoUsing AWS’s Dynamo package directly required too much code. This library also supports method chaining.
Environment variablescaarlos0 envGo tags can make code concise and readable. This library makes environment variables easy to load.

Redis cache

func SetCache(key string, obj interface{}, expiration time.Duration) error {
  err := getCodec().Set(&cache.Item{
    Key:        key,
    Object:     obj,
    Expiration: expiration,
  })
  return err
}

func GetCache(key string, obj interface{}) error {
  return getCodec().Get(key, obj)
}

MySQL

var config model.DeviceContentConfig
env.GetDatabase().Where(&model.DeviceContentConfig{DeviceId: deviceId}).FirstOrInit(&config)

Dynamo

if err := env.GetDynamoDb().Table(env.Config.DynamoTableProfile).Get(keyId, deviceId).All(&profiles); err == nil && len(profiles) > 0 {
  ...
}

Environment variables

var (
  Config     = ServerConfigStruct{}
  onceConfig sync.Once
)

type (
  ServerConfigStruct struct {
    ServerEnv  string `env:"SERVER_ENV"`
    LogLevel   string
....
  }
)

func LoadServerConfig(configDir string) {
  onceConfig.Do(func() {//최초 한번반 호출되도록
    env.Parse(&Config)
  }
}

Unit tests

Setting up the environment

Tests needed independent, isolated Redis, MySQL, and Elasticsearch environments, so we created a dedicated Docker setup. We placed test cases in a separate package, as shown below.

package buzzscreen_test

var ts *httptest.Server

func TestMain(m *testing.M) {
  ts = tests.GetTestServer(m)
  // 환경 시작
  tearDownElasticSearch := tests.SetupElasticSearch()
  tearDownDatabase := tests.SetupDatabase()

  code := m.Run()    // 여기서 작성한 TestCase 들 실행
  // 환경 종료
  tearDownDatabase()
  tearDownElasticSearch()
  ts.Close()

  os.Exit(code)
}

For mock servers, we implemented the http.RoundTripper interface and assigned it to the http.Client’s Transport field. Here is an example test setup:

httpClient := network.DefaultHttpClient
mockServer := mock.NewTargetServer(network.GetHost(MockServerUrl))
.AddResponseHandler(&mock.ResponseHandler{
  WriteToBody: func() []byte {
    return []byte(mockRes)
  },
  Path:   "/path",
  Method: http.MethodGet,
})
clientPatcher := mock.PatchClient(httpClient, mockServer)
defer clientPatcher.RemovePatch()

Unit testing is a large topic, so I’ll cover it in more detail in a future post.

Infrastructure

Splitting API requests

AWS Application Load Balancer

We first tried AWS Application Load Balancer, or ALB, to send allocation requests to the Go server while leaving all other requests on the existing Django server. ALB’s path-based routing let us send only allocation API calls to Go.

Source: Amazon DevOps Blog, Introducing Application Load Balancer

We didn’t keep this setup for long. Adding another server configuration and an ALB in front increased the resources needed to manage it, prompting us to look for a lower-overhead approach.

Using Docker and nginx

Was the Go server an independent microservice, or a performance-focused module extracted from the Django server? The latter felt more accurate, so managing Go and Django together made more sense. With Docker, an nginx container acts as a proxy, routing requests by path to either the Go or Django container.

Closing thoughts

Small beginnings, substantial results

Even moving a single API reduced the allocation API’s CPU requirements to roughly one-third, while server instance costs fell to about 1/2.5 of their previous level.

The CPU usage of each of the four existing Django instances fell by about 13%, while the Go instance used about 17%: 17 / (13 × 4) ≈ 1 / 3.

The results were encouraging enough that we subsequently moved several more APIs to Go and began implementing new APIs directly in the Go environment. What started as porting one frequently called API to another language grew into a substantial effort, including changes to server infrastructure and the adoption of containers. We completed it thanks to help and advice from many colleagues. We’d love to welcome you to an environment where experiments like this can succeed. Questions and suggestions about Go are welcome too!

All articles