AI-translated. Read the original in KoreanEmbedded slides, videos and images remain in their original language.
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
| Component | Library | Why we chose it |
|---|---|---|
| Network | Gin | Network performance was our top priority for a web server. We chose Gin after reviewing its benchmarks. |
| Redis & cache | go-redis | Performance was again the main criterion. |
| MySQL | Gorm | An ORM is hard to do without. Gorm supports multiple databases and offers method chaining. |
| Dynamo | guregu dynamo | Using AWS’s Dynamo package directly required too much code. This library also supports method chaining. |
| Environment variables | caarlos0 env | Go 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!
