Olivine: Building a Redis-Compatible Service in Go. · 章節目錄

第 1 章

[Day 1] Project initialization

Setup Go module

“Go module” is a dependency management for Go, it is a built-in feature after Go 1.13.

There’s a good tutorial from Go team: Tutorial: Create a Go module.

I initialized a go module by go mod init olivine in root of project, and it generated go.mod automatically.

Note: There’s a lot of project using github.com/{username}/{repo_name} as go module name, it works well for most of projects and make others access GitHub repository directly and easy.

The main function

The main function is a special function which means “entrypoint of a program”, I created cmd/olivine/main.go.

package main

import "fmt"

func main() {
	fmt.Println("Hello World")
}

We can run this code directly by go run ./cmd/olivine, you can see it shows “Hello World” on stdout.

$ go run ./cmd/olivine
Hello World

Hint: You can put main funcion in any folder under go module with package main even in project root. But I like to put binary source code under cmd/{binary_name}, it helps me manage projects by folder structures.

The Makefile

The make is a build tool for software development, it can use Makefile as a set of commands to help developers configure, compile, … and more!

All you need is create a Makefile in project root:

BIN_DIR := bin
CMD_DIR := cmd

build:
	@echo "Building binaries..."
	@mkdir -p $(BIN_DIR)
	go build -o $(BIN_DIR)/olivine ./$(CMD_DIR)/olivine

clean:
	@echo "Cleaning..."
	@rm -rf $(BIN_DIR)

We defined two commands: build and clean.

  • build: Create folder for binaries if not exists, and compiles go binary to executable.
  • clean: Cleanup binaries folder.

Now, use make build and it shows magic for you.

$ make build
$ ./bin/olivine # run the olivine binary
Hello World

Note: Don’t forget to add bin/ into .gitignore

A TCP Server

It’s time to build a TCP server!

There’s a good exaple from Go by Example: TCP Server.

package main

import (
	"bufio"
	"log/slog"
	"net"
	"strings"
)

func main() {
	slog.Info("Listen :16879 for TCP connections...")
	listener, err := net.Listen("tcp", ":16879")
	if err != nil {
		slog.Error("failed to listen tcp:", slog.Any("error", err))
		return
	}
	defer listener.Close()

	for {
		connection, err := listener.Accept()
		if err != nil {
			slog.Error("failed to accept connection:", slog.Any("error", err))
			continue
		}
		go handleConnection(connection)
	}
}

func handleConnection(conn net.Conn) {
	reader := bufio.NewReader(conn)
	message, err := reader.ReadString('\n')
	if err != nil {
		slog.Error("failed to read from conn:", slog.Any("error", err))
		return
	}

	ackmsg := strings.ToUpper(strings.ToUpper(message))
	response := "ACK: " + ackmsg
	if _, err := conn.Write([]byte(response)); err != nil {
		slog.Error("failed to write to conn:", slog.Any("error", err))
		return
	}
}

Let’s test this TCP server with nc:

$ echo "hello" | nc 127.0.0.1 16879
> ACK: HELLO
$ echo "hello from netcat" | nc 127.0.0.1 16879
> ACK: HELLO FROM NETCAT

Congratulations! This is your new TCP Server written in Go.