JavaRush /Courses /Go SELF /Real-world embed cases: help texts, sample CSV/JSON, and ...

Real-world embed cases: help texts, sample CSV/JSON, and templates

Go SELF
Level 44, Lesson 3
Available

1. Why embed “practical” files into the binary

When you’re learning, it feels normal to have an assets/ folder sitting next to the program. But as soon as you try to “send the program to a friend” or deploy it to a server, a fun quest begins: “did you definitely copy the folder?”, “is it next to the binary?”, “are permissions set correctly?”. embed solves this simply: resources become part of the binary, which means the program is “self-contained.”

Schematically, it looks like this:

flowchart LR
  A[Files in the repository
help.txt, samples/, templates/] --> B[go build] B --> C[Binary
with embedded resources] C --> D[Run the program] D --> E[fs.ReadFile / template.ParseFS
read resources from embed.FS]

In this lecture we’ll look at three very real-life scenarios where embedded resources truly make things easier: help text, “format examples” (sample CSV/JSON), and text templates.

2. Help text inside the program

When you build even the simplest CLI utility, you almost immediately need to explain to the user how to use it. At first, people print help directly as strings in main.go, but it quickly turns into a huge block of code where it’s hard to change anything. It’s much nicer to keep help as a separate file, and let embed simply embed it into the binary.

File structure

Suppose our learning app is called tasker (a simple task utility). Let’s add a resources directory:

tasker/
  main.go
  assets/
    data/
      help.txt

Let assets/data/help.txt be plain text, for example:

tasker — a mini task tracker

Usage:
  tasker help
  tasker sample json
  tasker sample csv

Tip:
  if you don't know what to do — start with tasker help

The assets package as a single entry point

Instead of declaring //go:embed directly in main.go, we’ll make a small assets package. This way embedding won’t “spread” across the project, and the rest of the code will work with neat functions.

assets/assets.go:


package assets

import (
	"embed"
	"io/fs"
)

//go:embed data
var embedded embed.FS

func FS() (fs.FS, error) {
	return fs.Sub(embedded, "data")
}

Note: we embed the directory data as a whole, and expose fs.FS that is already “re-rooted” to "data". This means that later we’ll refer to "help.txt", not "data/help.txt".

Reading help as text

Now let’s add a function that returns help as a string.

assets/help.go:

package assets

import (
	"fmt"
	"io/fs"
)

func ReadHelp() (string, error) {
	fsys, err := FS()
	if err != nil {
		return "", err
	}

	b, err := fs.ReadFile(fsys, "help.txt")
	if err != nil {
		return "", fmt.Errorf("read help.txt: %w", err)
	}
	return string(b), nil
}

What matters here is that we added context to the error ("read help.txt"), and preserved the cause via %w. This helps during debugging: you’ll see both “what you were doing” and “why it failed.”

The help command in main.go

For now, let’s keep a “naive CLI” based on os.Args (because a full-featured flag setup is a separate big topic, and we’re not touching it right now).

main.go:

package main

import (
	"fmt"
	"os"

	"tasker/assets"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("no command, try: tasker help")
		return
	}

	if os.Args[1] == "help" {
		help, err := assets.ReadHelp()
		if err != nil {
			fmt.Println("error:", err)
			return
		}
		fmt.Print(help)
	}
}

Help lives in a separate file, but the user gets it even if the binary is a single lonely file in a folder.

3. Sample CSV/JSON: “show me the format example”

When you design import/export (even if it’s “someday later”), people start asking a very practical question: “what should the file look like?”. You can answer in documentation, you can put it in README, but the most user-friendly option is to embed sample files and provide a command that prints them.

Important: in this lecture we are not parsing JSON or CSV. We use sample files as a “format example” that can be printed or saved. Parsing and strict format validation are separate topics.

Adding sample files to resources

Structure:

assets/
  data/
    samples/
      tasks.sample.json
      tasks.sample.csv

assets/data/samples/tasks.sample.json:

{
  "tasks": [
    { "title": "read about embed", "done": false },
    { "title": "have some tea", "done": true }
  ]
}

assets/data/samples/tasks.sample.csv:

title,done
read about embed,false
have some tea,true

A universal function for reading a text resource

To avoid rewriting the same thing every time, let’s make a small helper. It intentionally takes fs.FS so it can work not only with embedded resources (this is a good habit).

assets/readtext.go:

package assets

import (
	"fmt"
	"io/fs"
)

func ReadText(fsys fs.FS, name string) (string, error) {
	b, err := fs.ReadFile(fsys, name)
	if err != nil {
		return "", fmt.Errorf("read %q: %w", name, err)
	}
	return string(b), nil
}

The sample command

Add a function that returns the contents of sample files by the key "json" or "csv".

assets/samples.go:

package assets

import "io/fs"

func ReadSample(kind string) (string, error) {
	fsys, err := FS()
	if err != nil {
		return "", err
	}

	name := "samples/tasks.sample." + kind
	return ReadText(fsys, name)
}

And use it in main.go (I’ll show only the switch snippet to keep the example small):

switch os.Args[1] {
case "sample":
	if len(os.Args) < 3 {
		fmt.Println("usage: tasker sample json|csv")
		return
	}
	s, err := assets.ReadSample(os.Args[2])
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Print(s)
}

Now the user can run:

  • tasker sample json
  • tasker sample csv

and get an example on the spot. And you get fewer questions like “is done 0/1 or true/false?”.

Exporting a sample to disk

Sometimes it’s convenient for the user not just to see an example, but to get a file on disk to edit and use as a starting point. The embedded FS is read-only, but nothing stops you from reading an embedded resource and writing it to a regular file.

Minimal function “save the sample next to me”:

package main

import (
	"fmt"
	"os"

	"tasker/assets"
)

func saveSample(kind, outPath string) error {
	s, err := assets.ReadSample(kind)
	if err != nil {
		return err
	}
	return os.WriteFile(outPath, []byte(s), 0644)
}

Yes, this is a straightforward “write it directly” approach. In your project you likely already know more robust write schemes (via a temporary file and rename), but even this option is useful when you need to quickly provide a starter template to the user.

4. Templates and embed: text/template without string concatenation

Almost any utility has a moment when you want to print “human-readable” output: a task list, a report, a hint, a header for export. Beginners often do this with fmt.Println in a loop and a bunch of ifs, and that’s fine… until the format changes. As soon as you need to tweak the format, it’s better to have a template in a separate file. And this is where embed shines again: the template lives separately, but ships with the binary.

Creating a template file

Let’s add a directory:

assets/
  data/
    templates/
      tasks.txt.tmpl

File assets/data/templates/tasks.txt.tmpl:

Task list:
{{range .}}
- {{if .Done}}[x]{{else}}[ ]{{end}} {{.Title}}
{{end}}

This is a very “down-to-earth” template: it iterates over the data (range .), and inside checks the Done flag.

Data model: the minimal Task struct

Let’s keep a simple struct in our main.go for now:

type Task struct {
	Title string
	Done  bool
}

(You already know how to work with structs and slices—this is enough.)

Reading and parsing the template from fs.FS

In text/template, there’s a handy function template.ParseFS that takes an fs.FS. This is an ideal pair with embed.FS.

Let’s make a function in the assets package that returns a ready-to-use *template.Template.

assets/templates.go:

package assets

import (
	"io/fs"
	"text/template"
)

func ParseTasksTemplate() (*template.Template, error) {
	fsys, err := FS()
	if err != nil {
		return nil, err
	}

	return template.ParseFS(fsys, "templates/tasks.txt.tmpl")
}

Note: we parse the file at "templates/tasks.txt.tmpl" relative to the root returned by FS() (that is, relative to data/).

The render command

Now, in main.go, let’s add a command that shows “what the template looks like in action.” We’ll take the data directly from code to focus on embed + template, not on persistence.

case "render":
	tpl, err := assets.ParseTasksTemplate()
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	tasks := []Task{{Title: "read about templates", Done: false}}
	tpl.Execute(os.Stdout, tasks) // Task list: ...

If you want more predictable output, you can add fmt.Println() after Execute, but that’s just personal preference.

The key idea: the output format now lives in a .tmpl file, while the code is responsible for the data. This reduces “string gluing” in code and makes format changes easier: editing the template is simpler than rewriting printing logic.

Agreeing on resources in a project

When a project gets help files, sample data, and templates, the main thing is not to turn it into a “storage closet” where everything is there but nothing can be found. So even in small learning projects, it’s useful to establish a simple convention: where resources live, which file names are considered public, and which functions you use to access them.

A convenient “minimal contract” can even be written as a table so your brain doesn’t do extra work:

Resource type Where it lives under assets/data How we get it in code How we use it
Help text
help.txt
assets.ReadHelp()
tasker help
prints help
Sample JSON/CSV
samples/...
assets.ReadSample("json")
show the user a format example
Text template
templates/...
assets.ParseTasksTemplate()
render a list/report

If you keep //go:embed and the “resource root” in a single package, you almost automatically avoid chaos: the rest of the project doesn’t know how resources are laid out—it only knows the API.

5. Common mistakes

Error No. 1: keeping the help text directly in code and then being afraid to change it.
At first, fmt.Println("usage: ...") feels fast and convenient. But after a couple of weeks, help grows, and you start being afraid to “break the quotes” or forget \n. A help file + embed removes the problem: the text is edited like a normal document, and the program just prints it.

Error No. 2: smearing path strings across the project.
Today you write "templates/tasks.txt.tmpl", tomorrow somewhere else appears "data/templates/tasks.txt.tmpl", the day after—"assets/data/templates/...", and all of it starts living its own life. A good style is to keep the path structure inside the assets package, and expose functions like ReadHelp or ParseTasksTemplate so the rest of the code never builds paths by hand.

Error No. 3: trying to “embed everything,” including what should be user data.
Embedded resources are great for help, examples, templates, and static texts. But it’s a bad place for data that the user will actually change while the program runs: embed.FS is read-only, and you can’t “save new tasks into it.” Embed what belongs to the application distribution, not the user’s working data.

Error No. 4: ignoring errors while reading/parsing.
It’s tempting to write “well, it’s embedded, so it definitely exists.” But in practice, fs.Sub, fs.ReadFile, and template.ParseFS can all fail (for example, if there’s a typo in the template). So error handling here isn’t bureaucracy—it’s a way to quickly understand what exactly broke: the file, the path, the template, or the logic itself.

Error No. 5: turning a template into a “mini programming language” and moving logic into it.
Templates are for formatting, not business logic. If you start writing complex conditions, computations, and “almost algorithms” in a template, after a while it will become hard to read and even harder to test. It’s better to prepare the data in Go code and keep the template as dumb and predictable as possible.

1
Task
Go SELF, level 44, lesson 3
Locked
Help command
Help command
1
Task
Go SELF, level 44, lesson 3
Locked
Sample directory
Sample directory
1
Task
Go SELF, level 44, lesson 3
Locked
Task rendering
Task rendering
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION