Go Basic: Understanding Command Line, Environment Variables in Go

Building command-line applications and handling system configurations.

full lessons here👇:
https://programmerscareer.com/zh-cn/golang-basic-skill/

Generated by AI, there may be errors, for reference only

Topic: 1.1 Command Line Applications

Building command-line applications or CLI apps allow us to interact with our programs in a text-based interface. This can be really useful for creating scripts, automation, server-applications, or when you are SSH’ed into a server.

In Go, you can write powerful and efficient CLI applications and Go provides several libraries that make it even easier. For our examples, we’ll be working mostly with the “os” and “bufio” libraries.

The “os” package provides functions and types for interacting with the operating system in a platform-independent manner. The bufio package is used for buffered I/O operations, which can help to make I/O operations more efficient.

Here’s a simple example of a CLI app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main  

import (
"bufio"
"fmt"
"os"
)

func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your name: ")
name, _ := reader.ReadString('\n')
fmt.Printf("Hello, %s!", name)
}

In this script, we create a new reader wrapping the standard input (os.Stdin), prompt the user to enter their name, read the user input until a newline character, and finally print a greeting message to the console.

Your task now is to try to run and play around with this example. The next topic will expound on handling command-line arguments in Go applications.

Topic: 1.2 Handling Command Line Arguments

Command line arguments can provide flexible ways for a user to interact with your program. They might specify inputs, configurations, or any other runtime options that your program can use.

In Go, these command line arguments are available via the os.Args variable which is a slice of strings. Keep in mind that the first element in os.Args(i.e., os.Args[0]) is the name of the program itself.

Look at this simple example that prints all arguments:

1
2
3
4
5
6
7
8
9
10
11
12
package main  

import (
"fmt"
"os"
)

func main() {
for i, arg := range os.Args {
fmt.Printf("Arg %d: %s\n", i, arg)
}
}

You run it with arguments like this:

1
go run main.go firstArg secondArg

And this will print:

1
2
3
Arg 0: /tmp/go-build123456789/command-line-arguments/_obj/exe/main  
Arg 1: firstArg
Arg 2: secondArg

From here, we’re able to use the arguments in any way we’d like in our application.

Our next topic will focus on reading from and writing to the console, which helps to make our command-line applications more dynamic.

Topic 1.3 Reading from and Writing to the Console

As useful as command line arguments are, sometimes we need more interactive user input. This is when reading from standard input can be useful.

We’ve already seen an example of this in the first topic where we read a line of input from the user. Let’s explore this a bit more and start introducing writing to the console as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main  

import (
"fmt"
"bufio"
"os"
)

func main() {
reader := bufio.NewReader(os.Stdin)

// Reading a string from user until '\n'
fmt.Print("Please enter your name: ")
name, _ := reader.ReadString('\n')
// output to the console
fmt.Printf("Hi there, %s", name)
// Reading an integer value
fmt.Print("Enter your age: ")
var age int
fmt.Scanf("%d", &age)
fmt.Printf("You are %d years old!", age)

}

This script first reads a line of input from the user, providing a prompt for the user’s name. After confirming the name, it prompts for the user’s age and reads an integer from the console. The read value is then used to display the age to the user.

Your task is to review this script, run it, play around with it, and think about other ways you might want to read from or write to the console.

Topic: 1.4 Environment Variables

Environment variables are an excellent way to configure your program’s behaviour based on the environment it’s running in. They can hold values such as database connections, API keys, or anything else that you’d like to configure externally to your compiled program.

In Go, it’s easy to read and write environment variables using the os package. Check out this simple example:

1
2
3
4
5
6
7
8
9
10
11
package main  

import (
"fmt"
"os"
)

func main() {
os.Setenv("NAME", "John")
fmt.Println("Hello", os.Getenv("NAME")) // prints: Hello John
}

In the sample above, we’re using os.Setenv to create a new environment variable called NAME. Using os.Getenv, we retrieve this value and print it out.

Please note that environment variables set with os.Setenv are only available in the current process (i.e., your program). Once the process stops, the variable will no longer exist.

These variables are especially useful for configuring values that change based on the environment your app is running on, such as local, staging, or production.

Topic: 1.5 Creating Config Files

The configuration of an application is just as important as the application itself. It helps us to adjust the behavior of our application without changing our code. Configurations can be injected in many ways, but for this curriculum, we will create .env and .yamlconfiguration files which are commonly applied in many applications.

Let’s start with .env files first.

Dotenv files

In Go, a common package used to read .env files is godotenv. Create a .envfile and put this inside:

1
GREETING=Hello from an env file

Then, create a go file and use this content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main  

import (
"fmt"
"github.com/joho/godotenv"
"os"
"log"
)

func main() {
err := godotenv.Load(".env")

if err != nil {
log.Fatalf("Error loading .env file")
}
greeting := os.Getenv("GREETING")
fmt.Println(greeting) // prints: Hello from an env file

}

The godotenv.Load() function loads our environment file, and os.Getenv("GREETING")retrieves the value set in the env file. Run the Go script and observe the output.

Have a go at this and observe what’s happening. In general, .env files are recommended to use for local tests. When deploying into production, it’s advised to use actual environment variables due to security reasons.

YAML files

YAML (YAML Ain’t Markup Language) is a human-readable data serialization language, typically used for configuration files. Go has several packages to parse YAML, and we are going to use the most common one — go-yaml.

Firstly, let’s create a config.yaml file:

1
2
settings:  
greeting: Hello from a yaml file

Now, we will try to read the configuration from this file. In your go script, utilize the following codes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main  

import (
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
)

type conf struct {
Settings settings `yaml:"settings"`
}

type settings struct {
Greeting string `yaml:"greeting"`
}

func (c *conf) getConf() *conf {
yamlFile, err := ioutil.ReadFile("config.yaml")
if err != nil {
log.Printf("yamlFile.Get err #%v ", err)
}
err = yaml.Unmarshal(yamlFile, c)
if err != nil {
log.Fatalf("Unmarshal: %v", err)
}
return c
}

func main() {
var c conf
c.getConf()
fmt.Println(c.Settings.Greeting) // prints: Hello from a yaml file
}

In this script, we first define the structure of our YAML file. The conf type says that our root object has a Settings object, and the settings type says that the Settings object contains a Greetingstring. We load the file with ioutil.ReadFile and parse it with yaml.Unmarshal, then simply print the greeting message.

YAML files allow for much more complex structures compared to .env. This might be handy for larger applications with more sophisticated requirements.

Hopefully, this exercise gives you a good grasp on working with config files in Go. Remember, you mainly use these files to configure the behavior of your application without changing your code itself.

Topic 1.6 Configurations with Viper

While Go’s standard library and many other external packages provide utilities to manage environment variables and configuration files, handling complex configurations with ease can be a bit challenging.

Viper is one powerful library that comes with a rich set of features to deal with:

  • JSON, TOML, YAML, HCL, and Java properties config files
  • live watching and re-reading of config files
  • reading from environment variables
  • reading from remote config systems (etcd or Consul), and watching changes
  • setting default values
  • creating aliases

Let’s initialize a Viper object and set it up to read the environment variables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main  

import (
"fmt"
"github.com/spf13/viper"
"log"
)

func main() {
viper.SetEnvPrefix("myapp") // will be uppercased automatically
viper.BindEnv("id")
id := viper.Get("id") // case insensitive settings
fmt.Println(id)
}

As you can see, Viper allows us to do the same thing we would do with Go’s built-in os package but in a cleaner and less error-prone way.

Now, let’s assume we have the following config.yaml:

1
2
3
4
Port: 8080  
Db:
Username: "test-user"
Password: "test-pass"

We could access these configuration settings as follows:

1
2
3
4
5
6
7
8
9
10
11
viper.SetConfigName("config")  
viper.SetConfigType("yaml")
viper.AddConfigPath(".")

err := viper.ReadInConfig()
if err != nil {
log.Fatalf("Error while reading config file %s", err)
}
fmt.Printf("The application is running on port: %d\n", viper.GetInt("Port"))
fmt.Printf("DB User: %s\n", viper.GetString("Db.Username"))
fmt.Printf("DB Pass: %s\n", viper.GetString("Db.Password"))

In the example above, we use viper.GetStringand viper.GetInt to get our configuration values. Viper automatically provides us with the correct type.

Please try to work with this, and when you’re ready, we can move on to building a full application that utilizes all the concepts learned above.

Topic 1.7: Full Application — CLI Application with Flag Handling and Configuration

In this section, we will create a CLI application based on all the elements we’ve learned so far. It will demonstrate flag handling, parsing environment variables, and reading configuration from YAML files utilizing the Viper package.

Our application will be a basic HTTP Server. Let’s start coding!

Firstly, let’s define the configuration for our application:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Configuration struct {  
Server ServerConfiguration
Database DatabaseConfiguration
}

type ServerConfiguration struct {
Port int
}

type DatabaseConfiguration struct {
User string
Password string
}

To fetch the configuration from a file or the environment, we’ll create a LoadConfigurationfunction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func LoadConfiguration(configPaths []string) (Configuration, error) {  
var conf Configuration
viper.SetEnvPrefix("myapp")
replacer := strings.NewReplacer(".", "_")
viper.SetEnvKeyReplacer(replacer)
viper.BindEnv("server.port")
viper.BindEnv("database.user")
viper.BindEnv("database.password")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
for _, path := range configPaths {
viper.AddConfigPath(path)
}
if err := viper.ReadInConfig(); err != nil {
fmt.Fprintf(os.Stderr, "Note: Could not read config file: %s\n", err)
}
err := viper.Unmarshal(&conf)
return conf, err
}

This function tries to read the configuration from the environment and falls back to the configuration file if no environment variables were set.

Now, let’s use this function in our main function and run our HTTP server:

1
2
3
4
5
6
7
8
9
func main() {  
configPaths := []string{"/etc/myapp/", "$HOME/.myapp", "."}
config, _ := LoadConfiguration(configPaths)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to my website!")
})
fmt.Printf("Server running on port: %d\n", config.Server.Port)
http.ListenAndServe(fmt.Sprintf(":%d", config.Server.Port), nil)
}

In this snippet, we’re loading our configuration from either the environment variables or a file in one of configPaths. We then start our HTTP server on the configured port.

Make sure to replace "myapp" with your application’s name and also substitute real values in the configuration paths.

Try running this application — pass different parameters and observe the output.

Topic 1.8: Review and Practice

We’ve covered significant ground in this course. Let’s go over the key points you’ve learned:

  1. Command-Line Applications in Go: You’ve understood the fundamentals of building CLI applications in Go, leveraging the standard “os” and “bufio” libraries.
  2. Handling Command Line Arguments: You’ve learned how to manage command line arguments using the os package, along with understanding flags, arguments, and options.
  3. I/O Operations on Console: You’ve mastered reading from and writing to the console using the “os” and “bufio” packages.
  4. Environment Variables: We covered how to read and set environment variables using the os package and discussed why these environment variables are crucial.
  5. Creating Config Files: We dabbled in creating .env and .yaml files to store application configurations.
  6. Complex Configurations with Viper: You learned about handling complex configurations using the Viper package.
  7. Full Application: Using all the knowledge you’ve gained, you constructed a fully functional CLI application, demonstrating flag handling and environment variable configurations.

Now, let’s test your knowledge with some exercises:

  1. Expand on the difference between flags, arguments, and options in a CLI application. Provide an examples for each.
  2. Explain how you would utilize environment variables in an HTTP server application in Go. What type of information would you store in them?
  3. Write a brief piece of code demonstrating how you’d read a .yaml config file in a Go application.
  4. Create a small Go CLI application that takes a user’s name as a command line argument and greets the user.

These exercises will help cement the concepts you’ve learned.

Topic 1.9: Assessments

This session will assess your understanding and application of the topics we’ve covered. Don’t worry, these are designed to measure your comprehension and not to trick you. Take it as an opportunity to reflect on everything you’ve learned. Here we go:

  1. Command Line Applications: Explain what a Command Line Interface (CLI) is and why we choose to build applications for the command-line instead of a graphical user interface (GUI).
  2. Handling Command Line Arguments: Write a Go program that can accept command line arguments and print them.
  3. I/O Operations on Console: Write a Go program that reads an input string from the console and prints it back.
  4. Environment Variables: Write a Go program that can read an environment variable and print its value.
  5. Config Files: Explain the use of .env or .yamlfiles for storing configurations.
  6. Configurations with Viper: Write a Go program that reads a config value from a .yaml file using the viper library.
  7. Full Application: Consider an application that requires several configurations (like an API key or a URL). How would you use everything you have learned to manage this?

Take your time, think about the questions, and do your best to answer them. Remember, this is your opportunity to reflect on the progress you’ve made.

中文文章: https://programmerscareer.com/zh-cn/go-basic-14/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: If you choose to repost or use this article, please cite the original source.

Go Basic: Understanding HTTP Client, HTTP Server, Context, Processes, Signals, Exit in Go Go Basic: Understanding Testing and Benchmarking in Go

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×