Generating Go Code with Go Generate

Introduction

Let’s get straight to it.

You have a json schema/structure and you want to generate the struct for it. There are tools for this, such as JSON to Go, but here’s a simple implementation, as part of the introduction

We’ll create 4 files.

  1. post.json - to hold the json schema of the code we are trying to generate
  2. post.tpl - to hold the template with which the code will be generated from
  3. gen.go - go code to be used to generate the code
  4. gen.sh - script file to automate tasks after generation
  5. main.go - a simple program to run the generated code

Data Source

Let’s say you have this as a Post JSON. Create a file named post.json

{
  "title": "using go generate",
  "tags": ["go", "generate"],
  "draft": true,
  "created": 1552804059,
  "rating": 3.5
}

I intentionally used the values above to demonstrate different data types

Template

Next, we create a file post.tpl to represent the template that will be parsed, using go standard template spec

// Code generated by go generate; DO NOT EDIT.
// Generated at {{ .Timestamp }}
// Generated from {{ .JSON }}

package posts

type {{ .Name }} struct {
{{- range $jsonName, $val := .Fields }}
    {{(Title $jsonName)}} {{(TypeOf $val) }} `json:"{{$jsonName}}"`
{{- end}}
}

From the above template, we see that we need a couple of things.

  1. Timestamp - the unix timestamp value of the time the code was generated
  2. JSON - the json string that the code was generated from
  3. Name - the name we give to the struct we are trying to generate
  4. Fields - the fields generated from the parsing of the json file we will provide.

Generate

Next we create our gen.go file that will be used to generate the code we need. I’ve tried to comment on the code as much as possible.

You will notice a // +build ignore annotation at the beginning of the file. This will keep the file from being considered during build. In this case this is important for tools, and the fact that we are going to have a main.go file with a main func. You can read in build constraints documentation

I have also ignore all the errors for brevity.

// +build ignore

package main

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"os"
	"reflect"
	"strings"
	"text/template"
	"time"
)

func main() {
	// model represents the fields in a json object
	var model map[string]interface{}

	// we read the 'post.json' file in bytes
	b, _ := ioutil.ReadFile("post.json")

	// unmarshal the json bytes into the model
	_ = json.Unmarshal(b, &model)

	// trim spaces from json
	var buffer bytes.Buffer
	_ = json.Compact(&buffer, b)

	// define the data needed by the template
	data := struct {
		Timestamp int64
		JSON      string
		Name      string
		Fields    map[string]interface{}
	}{
		Timestamp: time.Now().Unix(),
		JSON:      buffer.String(),
		Name:      "Post",
		Fields:    model,
	}

	// define the functions needed by the template
	funcMap := template.FuncMap{
		"Title": strings.Title,
		"TypeOf": func(v interface{}) string {
			if v == nil {
				return "string"
			}
			return strings.ToLower(reflect.TypeOf(v).String())
		},
	}

	// create a template
	tpl := template.Must(template.New("post.tpl").Funcs(funcMap).ParseFiles("post.tpl"))

	// create the package directory
	_ = os.Mkdir("post", os.ModePerm)

	// create the file where the code will go to
	file, _ := os.Create("post/post.go")

	defer file.Close()

	// parse the template
	_ = tpl.Execute(file, data)
}

We could test this file by running

go run gen.go

This will produce the following file post/post.go

// Code generated by go generate; DO NOT EDIT.
// Generated at 1552820501
// Generated from {"title":"using go generate","tags":["go","generate"],"draft":true,"created":1552804059,"rating":3.5}

package posts

type Post struct {
    Created float64 `json:"created"`
    Draft bool `json:"draft"`
    Rating float64 `json:"rating"`
    Tags []interface {} `json:"tags"`
    Title string `json:"title"`
}

The last thing that needs to be done with this generated file is format it.

go fmt post/post.go

Formatted

// Code generated by go generate; DO NOT EDIT.
// Generated at 1552820501
// Generated from {"title":"using go generate","tags":["go","generate"],"draft":true,"created":1552804059,"rating":3.5}

package posts

type Post struct {
	Created float64       `json:"created"`
	Draft   bool          `json:"draft"`
	Rating  float64       `json:"rating"`
	Tags    []interface{} `json:"tags"`
	Title   string        `json:"title"`
}

The above process can be achieved in two steps, which is why we will create a file gen.sh to run the two commands for us. This can also be achieved with a make file.

#!/usr/bin/sh
set -e

go run gen.go
go fmt post/post.go

Change the file permissions to allow execution

chmod +x gen.sh

go:generate

The fun part, now all that needs to be done is ensure go generate runs.

To do this, let’s not create our main.go file

package main

//go:generate sh gen.sh

import "fmt"

func main() {
	fmt.Println("generated")
}

The //go:generate annotation is what allows the code generation to happened.

You can delete the post directory, and then run

go generate

This will generate the code as we tested earlier and then run go fmt according to the script.

Happy Coding

golang  go 

comments powered by Disqus