In this challenge, you will create a simple Go program that will ask users for some input and then will store all the information in Vault. By the end of this challenge, you should have a very good understanding of many Golang concepts and should have enough knowledge to start developing Golang programs on your own or contribute to existing open source projects. This is not, however, a Dojo on Vault, which means that even though you will learn some of the basics, we will not dive in as much.
In this challenge, you will probably come across some code design decisions which might not necessarily follow industry best practices. Since we are focusing more on introducing Golang concepts rather than building robust systems, sometimes we might need to take an easier approach to systems design so people can learn more efficiently.
First, we will spend some time (not much, though!) setting up your environment. Then, there will be number of phases where each one of them will go over one or more Golang concepts. And to make sure you're on the right path, there will be Definition of Done sections to show you how to test your solution. Finally, you will see some For discussion sections (which are optional and can be skipped). The goal of these sections is to create a discussion between the team members and the organizers about a certain topic.
Before you start developing any code, let's set up our Golang and Vault environments. You will not need to install anything in your machine apart from Docker. As a first step, create a folder called golang-dojo
anywhere under your user's directory. This will be where you will develop the code.
Next, run the command (<HASH>
is not part of the command, that's just the output of the docker command):
$ docker run --rm -d -v <FULL-PATH-TO-GOLANG-FOLDER>:/go/src/golang-dojo slalomdojo/env:golang
<HASH>
PS 1: you need to specify /go/src/golang-dojo
as the destination inside the container so your code can be compiled according to this challenge's intructions
PS 2: if you are on Windows and the directory mounting is not working, make sure you have configured Docker to allow sharing the C: drive with containers. If it still doesn't work, reach out to one of the organizers of the event so we can discuss options.
The command above will give you back a hash that can be used to jump into the container:
docker exec -it <HASH> sh
Once inside the container, you should be able to run go
and get the following:
/go # go
Go is a tool for managing Go source code.
Usage:
go <command> [arguments]
The commands are:
bug start a bug report
build compile packages and dependencies
clean remove object files and cached files
doc show documentation for package or symbol
env print Go environment information
fix update packages to use new APIs
fmt gofmt (reformat) package sources
generate generate Go files by processing source
get add dependencies to current module and install them
install compile and install packages and dependencies
list list packages or modules
mod module maintenance
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet report likely mistakes in packages
Use "go help <command>" for more information about a command.
(...)
Now let's export two environment variables which will be needed later when connecting to the Vault server. Open a new terminal, and run docker logs
to retrieve the logs from the container you just ran:
docker logs <HASH>
That will output quite a bit of log lines generated by Vault:
==> Vault server configuration:
Api Address: http://127.0.0.1:8200
Cgo: disabled
Cluster Address: https://127.0.0.1:8201
Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
Log Level: info
Mlock: supported: true, enabled: false
Recovery Mode: false
Storage: inmem
Version: Vault v1.3.0
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.
You may need to set the following environment variable:
$ export VAULT_ADDR='http://127.0.0.1:8200'
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: RBBbPkKAS5r9MOqSorBkfVCdd9GZeoTILZdm0i+b5ZI=
Root Token: <ROOT TOKEN>
Development mode should NOT be used in production installations!
==> Vault server started! Log data will stream in below:
2019-11-22T18:45:27.098Z [INFO] proxy environment: http_proxy= https_proxy= no_proxy=
2019-11-22T18:45:27.098Z [WARN] no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2019-11-22T18:45:27.106Z [ERROR] core: no seal config found, can't determine if legacy or new-style shamir
2019-11-22T18:45:27.106Z [ERROR] core: no seal config found, can't determine if legacy or new-style shamir
2019-11-22T18:45:27.106Z [INFO] core: security barrier not initialized
2019-11-22T18:45:27.107Z [INFO] core: security barrier initialized: stored=1 shares=1 threshold=1
2019-11-22T18:45:27.112Z [INFO] core: post-unseal setup starting
2019-11-22T18:45:27.123Z [INFO] core: loaded wrapping token key
2019-11-22T18:45:27.123Z [INFO] core: successfully setup plugin catalog: plugin-directory=
2019-11-22T18:45:27.123Z [INFO] core: no mounts; adding default mount table
2019-11-22T18:45:27.126Z [INFO] core: successfully mounted backend: type=cubbyhole path=cubbyhole/
2019-11-22T18:45:27.127Z [INFO] core: successfully mounted backend: type=system path=sys/
2019-11-22T18:45:27.127Z [INFO] core: successfully mounted backend: type=identity path=identity/
2019-11-22T18:45:27.129Z [INFO] core: successfully enabled credential backend: type=token path=token/
2019-11-22T18:45:27.129Z [INFO] core: restoring leases
2019-11-22T18:45:27.130Z [INFO] rollback: starting rollback manager
2019-11-22T18:45:27.130Z [INFO] expiration: lease restore complete
2019-11-22T18:45:27.131Z [INFO] identity: entities restored
2019-11-22T18:45:27.131Z [INFO] identity: groups restored
2019-11-22T18:45:27.131Z [INFO] core: post-unseal setup complete
(...)
You will need two things from the log output: the Root Token and the Vault Address (e.g. http://127.0.0.1:8200). Go back to the container you span up and export two environment variables:
/go # export VAULT_ADDR='http://127.0.0.1:8200'
/go # export TOKEN=<ROOT TOKEN>
Now we're ready to start!
Whenever you're developing Go code, the code should be part of a workspace. And to quote the Go documentation, a workspace is a directory hierarchy with two directories at its root:
src
contains Go source files andbin
contains executable commands.
The go
tool builds and installs binaries to the bin
directory.
The src
subdirectory typically contains multiple version control repositories (such as for Git or Mercurial) that track the development of one or more source packages.
Here's an example:
bin/
hello # command executable
outyet # command executable
src/
github.com/golang/example/
.git/ # Git repository metadata
hello/
hello.go # command source
outyet/
main.go # command source
main_test.go # test source
stringutil/
reverse.go # package source
reverse_test.go # test source
golang.org/x/image/
.git/ # Git repository metadata
bmp/
reader.go # package source
writer.go # package source
... (many more repositories and packages omitted) ...
How does the go compiler know where in your system is the "Go Workspace"? Through an environment variable called GOPATH
. It defaults to a directory named go
inside your home directory, so $HOME/go
on Unix/Mac, and %USERPROFILE%\go
(usually C:\Users\YourName\go
) on Windows.
If you'd like to see which environment variables the Go compiler uses, run go env
.
Next, let's list all directories under /go
:
/go # ls -l /go
total 8
drwxrwxrwx 2 root root 4096 Nov 1 20:22 bin
drwxrwxrwx 1 root root 4096 Nov 22 18:45 src
bin src
You will see that we already have a bin
and src
directory, which means you do not have to create anything at this point. Next, you will learn how to leverage this folder structure to compile code and install binaries
How can we learn a new programming language without creating a Hello World program? Let's start with that.
If you were able to mount a volume into the container successfully, you should see a folder called golang-dojo
inside src
:
/go # ls /go/src
golang-dojo
That's where you will be able to find your code. But again! You will not develop the code inside the container. Since this directory exists in your local computer, that's where you should develop the code. In your local computer, open a text editor or IDE in the golang-dojo folder.
However, if you wish to develop code inside the container, install vim or nano with the command: apk add --no-cache <vim/nano>
.
Now that you have the golang-dojo
folder open in a Text Editor or IDE (or vim inside the container), create a file called main.go
.
Paste the following code:
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
Before we compile it, let's understand what's all that. If you already understand, skip the next 4 sections.
In Go, every single go file belongs to a package. In this case, our main.go
belongs to the main
package. The main
package is a special package because it indicates that the code there is an executable. The Go CLI will not run programs that do not contain a package main statement at the very top. If we were to call this package something else, we'd get an error like:
go run: cannot run non-main package
Another thing that is worth noting is that the very first line of a Go code should be the package to which the code belongs. If you were to start this Hello World program with import "fmt"
, the Go CLI would throw an error saying:
main.go:1:1: expected 'package', found 'import'
So, to summarize: regardless if you're developing a one-file program or a complex Golang API, the very first instructions that you want your program to run should be in the main
package.
As the name implies, the keyword import
is used to import other packages into your program. Here we are importing a package called "fmt", which contains tons of functions related to formatting I/O (with functions analogous to C's printf and scanf).
We will use one of the functions of this package to be able to print in the terminal.
In Go, that's how a very basic function declaration looks like: the keyword func
followed by the function name and any parameters it might take (no parameters needed in this case) and the return value (which is non-existent here for the main function).
And as you might've guessed, main()
is a special function (the same way package main
is): it is the entry point of an executable program.
As mentioned before, fmt
is a package that contains functions related to I/O. If you'd like to use a function defined in a package, you prefix that function name with <package-name>.
. So, to use a function called Println
which has been defined in the package fmt
, the syntax is fmt.Println()
You will see, however, that it is possible to invoke a function without specifying the package name, but we'll get to the nitty-gritty of packages/imports soon when we develop our own packages.
Ok! Now that you have an understanding of this Hello World program, it's time to run it.
First, to run this program without generating a binary, use the go run
command:
/go/src/golang-dojo # go run main.go
Hello World!
Next, let's generate a binary so we can execute it. The command below compiles main.go
into a binary called dojo
:
/go/src/golang-dojo # go build -o dojo main.go
Now, run ./dojo
- you should get the same output.
Finally, remember that bin
directory? If you'd like to compile and send the binary to the bin
folder, use the go install
command without specifying main.go:
/go/src/golang-dojo # go install
This command will compile main.go and generate a binary that has the same name as the folder where main.go is - golang-dojo
. If you run ls /go/bin
, you should see a golang-dojo
binary there. Execute that binary and you should have the same "Hello World" output.
The "Hello World" program was purely a warm-up! Things will start getting more serious now...
As mentioned at the beginning, in this challenge you will write a Go program that asks users for some input and saves that information. So let's focus first on getting user input from the terminal.
Here's the information you will ask the user:
- First Name
- Last Name
- Date of Birth
- Nationality
As a first step, let's create variables to store all this information.
A very useful website when learning Go is Go By Example. In this website, there are tons of annoted example programs that explain how to use many of the Golang concepts. To learn how to declare variables, check the Variables page.
You should declare 5 variables: firstName, lastName, dob, nationality and email. All of these variables should be of type string
.
Now, when it comes to reading input from Standard Input, things can get a little tricker. There are many ways to read input directly from the terminal, but let's try to keep it easy for now. The following snippet of code reads characters from the terminal (including white spaces) until it finds a newline character:
var reader = bufio.NewReader(os.Stdin)
myVar, _ = reader.ReadString('\n')
myVar = strings.Replace(myVar, "\n", "", -1)
Don't worry too much for now what this is doing, but use the code above to get all the information required from the user and save the data in those 5 variables you declared.
PS: to make you program more human-friendly, use fmt.Println()
to print the questions in the terminal.
After reading all the variables, print the following:
Here is what we know about you:
First name: <FIRST NAME> # use firstName variable
Last name: <LAST NAME> # use lastName variable
Date of Birth: <DATE OF BIRTH> # etc...
Nationality: <NATIONALITY>
Email: <EMAIL>
Before you try to compile the code, note that the snippet above uses 3 packages: bufio
, os
and strings
. You will have to import those packages.
Here's how your program should look like at the end of Phase 2:
/go/src/golang-dojo # go run main.go
Tell us about yourself:
First Name? John
Last Name? Doe
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com
Here is what we know about you:
First name: John
Last name: Doe
Date of Birth: 13th September 1980
Nationality: Canadian
Email: johndoe@gmail.com
The exact wording doesn't matter, as long as you were able to read characters from terminal, store them in variables and use those variables to print the information gathered.
Naturally, whenever you develop a complex program, there will be tons and tons of packages. Packages that are native to the language and packages that you will have to create yourself.
Here's what you need to do next:
- Create a folder called
internal
at the root level ofgolang-dojo
- Inside the
internal
folder, create a folder calledperson
- Inside the
person
folder, create a file calledperson.go
.
The goal now is to get all that code that reads characters from the terminal and put it into a package called person
. In person.go
, declare a package called person
.
Next, declare a function called GetPersonInfo()
. Note that the very first letter needs to be uppercase. We'll explain why in a bit. Then, copy and paste all the code used to read input from the terminal into this new function - but leave out the code that prints the information in the terminal.
At this point, your function should look something similar to the following:
func GetPersonInfo() {
<variable declaration>
var reader = bufio.NewReader(os.Stdin)
<read first name>
<read last name>
<read dob>
<read nationality>
<read email>
}
Since the code to print the information to the terminal is not present in this function, if we call this function from main()
, we will not get anything in return and therefore we will not be able to print anything. Let's work on that a little bit.
First, let's learn about maps
. map
works the same way as other programming languages: it maps keys to values. Take at look at this example: https://gobyexample.com/maps.
At the beginning of the GetPersonInfo()
function, declare a map where both keys and values will be of type string
. Now that you have a map, you don't need variables anymore. Use the map to store the input from the terminal.
Once you've changed the code to use maps, return
that map so whoever is calling this function can have access to the information gathered.
PS: if you return a value, you need to indicate in the function declaration what's the type of the information being returned. Take a look at this example.
As a last step, go back to main.go
. The first thing you need to do is to import the new package (person
) that you created.
Read the following links to understand how to import your package in main.go
:
Once you were able to import your package, declare a variable that will receive the returned data from GetPersonInfo()
. Then, print the information in the terminal.
What would happen if the name of the function was getPersonInfo()
instead? (lower case g
)
The definition of done is exactly the same as Phase 2.
So far, we've been asking for the user's first and last name, date of birth, nationality and email. All this information is of type string
. But what if we were to introduce something of type int(eger)
? Like age
? Then, in that case, we should avoid using a map
because we would have values of 2 different types: string
and int
(it's still possible to use a single map with different value types, but that's an advanced concept which we won't get into at this moment).
Leaving advanced concepts aside, if you wanted to use a map
, you'd have to have a separate map
or variable for the age
. And that would not be the best approach here. Therefore, let's learn about Structs.
Based on the example above (see the Structs link), create a Struct called Person
with 6 fields (including the age
). Then, create a NewPerson()
function that returns a Person
. The function signature should be the following:
func NewPerson(firstName string, lastName string, age int, dob string, nationality string, email string) *Person
PS 1: By convention, the function that returns a struct is named New[Struct-Name]
, so in this case NewPerson
PS 2: You will notice that at the end, we're returning *Person
instead of just Person
. Learn here the difference between pass by reference and pass by value
Once you're done with the NewPerson()
function, create a Print
function that will print the information gathered.
Attention: If you create a function with the following signature:
func Print()
You won't be able to access the struct's fields. Give a quick read to this article to learn how to create "method" for the struct. Then, create a Print()
method that will print all of the struct's fields in the same format as before.
Now, how about that GetPersonInfo()
function that we created before? We won't need it anymore. That was just to introduce you to packages. You can move the code back to main()
.
Before you're done with Phase 4, in main()
you will need to read the age of the user, which is an integer. Therefore, the code you were using to read strings will not work. Use the snippet below to read integers:
_, err := fmt.Scanf("%d\n", &age)
if err != nil {
panic("<INSERT AN ERROR MESSAGE HERE LETTING THE USER KNOW THAT IT WASN'T POSSIBLE TO READ THEIR AGE>")
}
The code above uses fmt.Scanf()
to read the age. If you try to use Scanf
to read strings, beware that it will not read a string that contains space. So, to make things simple here, use the bufio
library to read strings with spaces and the Scanf
function to read integers.
Once you've read all the input from the terminal, create a Person
and tell it to print it's personal information.
-
When and why would you use a struct by value instead of by reference? Read this article.
-
What does the syntax
_, err := fmt.Scanf("%d\n", &age)
mean?
The definition of done is the same as Phase 2.
Instead of just asking for the user's personal information like name and email, let's also ask about the user's car. Here's the information that you should get from the user: car's make, model, year and colour.
The approach you will take here should be similar to what you've done before in Phase 4: create a Struct called Car
, declare all the fields and create the same methods you created for the Person
struct. You do not need to create these structs in the same file. In fact, you should really create separate files and separate packages. Organize your directories/files in the following way:
internal/
├── car
│ └── car.go
└── person
└── person.go
person.go
should belong to the person
package and car.go
should belong to the car
package.
Here's how you program should work:
Tell us about yourself:
First Name? John
Last Name? Doe
Age? 30
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com
Here is what we know about you:
First name: John
Last name: Doe
Age: 30
Date of Birth: 13th September 1980
Nationality: Canadian
Email: johndoe@gmail.com
Now tell us about your car:
Make? Cadillac
Model? XTS
Year? 2020
Color? Red
Here is what we know about your car:
Make: Cadillac
Model: XTS
Year: 2020
Color: Red
Now that we are able to collect some information from our user, let's work on saving all this data to Vault.
Before we get started, we need to enable a custom kv Secret Engine on Vault.
PS: If you feel you'd benefit from a short tutorial on Vault, here's the official getting started documentation
In the container, run the following command:
# vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_9804bbe6 per-token private secret storage
identity/ identity identity_0191b96f identity store
secret/ kv kv_b05336d2 key/value secret storage
sys/ system system_50ef7af2 system endpoints used for control, policy and debugging
PS: If you got the following error: Error listing secrets engines: Get https://127.0.0.1:8200/v1/sys/mounts: http: server gave HTTP response to HTTPS client
, that means you probably haven't exported VAULT_ADDR
and TOKEN
. Go back to the bottom of the Getting Started section and export these variables.
If you take a look at the Type
column, you will see that the third line is a kv
secret engine. This is the default kv
secret engine that comes with Vault. What we'll do is to enable a custom one for this challenge and we will not use the default one.
To enable a custom kv
secret engine, run the command vault secrets enable -path=dojo/ kv
. Then, if you run vault secrets list
one more time, you should get this:
# vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_9804bbe6 per-token private secret storage
dojo/ kv kv_2af64e61 n/a
identity/ identity identity_0191b96f identity store
secret/ kv kv_b05336d2 key/value secret storage
sys/ system system_50ef7af2 system endpoints used for control, policy and debugging
You should have a kv
secret engine at the path dojo/
.
There are 3 main ways throught which we can interact with Vault:
- The Vault CLI (which we just tried)
- The Vault API (HTTP)
- The Golang Vault SDK
Since this is a Golang Dojo, it makes sense we learn how to use the Golang Vault SDK as well. Here's the documentation
First, let's create a client struct that will be responsible for interacting with Vault. Here's what you need to do to get started:
- Under the
internal
folder, create a folder calledvault
- Under the
vault
folder, create aclient.go
file - In
client.go
, create a struct calledVaultClient
and a function calledNewVaultClient()
- The
VaultClient
struct should have 1 field only - and I'll leave it up to you to decide which one. - The
NewVaultClient()
function should receive as parameters: vaultAddress and token (both strings) and should return multiple values:*VaultClient
and error (learn here how to return multiple values in a single function)
The function NewVaultClient()
is not as straightforward as just returning a new VaultClient struct, so let's discuss it a little bit.
To have an idea of what needs to happen in the NewVaultClient()
function, have a look at the InitVault() function in this StackOverflow answer.
You will just have to modify the code slightly to return *VaultClient and error. Also, the StackOverflow answer declares a global variable var VClient *api.Client
. Do not use a global variable. Think how you can leverage the struct for that.
At this point, your Vault client is able to initialize a connection with Vault, but it doesn't save any data just yet. Let's work on that next.
Create a method for the VaultClient
struct called SaveSecret()
that returns an error
if it wasn't possble to save the secret. This method should receive one parameter only - a Secret.
Ok, hold on. So far, we have created 3 structs: Person
, Car
and VaultClient
. What is this Secret
?
Let's think about it together. You want VaultClient
to be able to save secrets to Vault. Now, do you think VaultClient
should care about the secret type? If you define a function like so:
func (vc *VaultClient) SaveSecret(p Person) error
that means we'll only be able to save a secret of type Person
, but not Car
. So, the idea is to create a new type called Secret
, that will be the base for multiple secret types (like Person
and Car
).
If you come from the Object-Oriented world, you probably heard of Interfaces:
An interface is a programming structure/syntax that allows the computer to enforce certain properties on an object (class). For example, say we have a car class and a scooter class and a truck class. Each of these three classes should have a start_engine() action. How the "engine is started" for each vehicle is left to each particular class, but the fact that they must have a start_engine action is the domain of the interface.
Even though Golang is not Object-Oriented like Java, for example, the concept still applies, although a bit differently. In essence, what you need to do is to create an Interface named Secret
, define function(s) within that interface (which we will talk about soon), then modify both Person
and Car
structs to "conform" to this interface (while in Java you need the keyword implements
to say that a class implements an interface, in Go as long as the struct implements all functions of the interface, Go automatically considers that the struct "conforms" to the interface - you don't need to explicitly use a keyword like in Java).
Take a look at this example to understand how interfaces work.
Your Secret
interface should declare 3 functions:
PrintSecret()
GetData() map[string]interface{}
Type() string
PrintSecret()
- Remember that our Person
and Car
structs had a function called Print()
? Let's rename that and call it PrintSecret()
.
GetData() map[string]interface{}
- The function GetData should return a map[string]interface{}
(this is a map where the keys are strings and the values are of type interface{}
- don't worry about that for now). This map should use all the struct's fields as keys. For example, for the struct Person
, this is how it would look like:
map[string]interface{}{
"firstName": <first-name>,
"lastName": <last-name>,
(...)
}
How about age
? age
is an integer and not a string like all other fields. Can you create a map[string]interface{}
mixing string and integers? Yes, you can. Find out why here.
Type() string
- This function should return either "person" or "car" (note that we're talking about strings here, not structs), depending on the secret type. This will be useful when we save the secret in Vault.
Now that you created the Secret
interface and implemented the 3 methods above in both Person
and Car
structs, let's go back to the implementation of the SaveSecret()
function.
So, again, declare a function for the VaultClient
struct called SaveSecret()
that receives a single parameter of type Secret
(interface) and returns an error
.
I will leave it up to you to figure out how to save a secret in Vault. These are the requirements your SaveSecret()
function need to comply with:
- If you're saving a
Person
, the path for the secret should be/dojo/person
- If you're saving a
Car
, the path for the secret should be/dojo/car
- After saving the secret, print to the screen the following message:
Secret of type [person/car] saved succesfully!
Before you compile, you will have to download the Vault SDK. This is the GitHub URL for the SDK: github.com/hashicorp/vault/api
. Now, to download it, it's very simpe. Run the command:
# go get github.com/hashicorp/vault/api
When it's done, you should be able to compile your program.
This is how the interaction with the program should look like:
Tell us about yourself:
First Name? John
Last Name? Doe
Age? 30
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com
Here is what we know about you:
First name: John
Last name: Doe
Age: 30
Date of Birth: 13th September 1980
Nationality: Canadian
Email: johndoe@gmail.com
Now tell us about your car:
Make? Cadillac
Model? XTS
Year? 2020
Color? Red
Here is what we know about your car:
Make: Cadillac
Model: XTS
Year: 2020
Color: Red
Secret of type person saved succesfully!
Secret of type car saved succesfully!
Once both secrets are saved successfully in Vault, you should be able to see them:
# vault kv get /dojo/person
======= Data =======
Key Value
--- -----
age 30
dob 13th September 1980
email johndoe@gmail.com
firstName John
lastName Doe
nationality Canadian
# vault kv get /dojo/car
==== Data ====
Key Value
--- -----
color Red
make Cadillac
model XTS
year 2020
If you've made it at this point, congrats!!! But we're not done yet! Let's look into one more Golang concept: Go Routines!
So far, all of your code has been executing in a sequence. When you call the SaveSecret()
function twice to save the Person
and Car
secrets, the go runtime will execute each call at a time. This is probably a bit overkill for this use case, but what we are going to do now is to execute those 2 function calls concurrently/in parallel. If you not sure about the difference between concurrency and parallelism, read this short answer on StackOverflow.
Whether your code will be executing concurrently or in parallel, that's up to Go and your computer's hardware. The code you will be developing is exactly the same for both cases. So let's get to it.
Whenever you run a Go program, you're creating a process. You can think of Go Routine as an engine inside this process that executes code. When you launch a program and Go executes the main()
function, that's a Go Routine. So whenever a Go program executes, there's at least 1 Go Routine running - the main()
function.
To create more Go Routine, it is as easy as using the go
keyword. Take a look at the examples below.
The code below will execute each function one by one:
getUrlContents("https://www.startpage.com/")
getUrlContents("https://protonmail.com/")
getUrlContents("https://nordvpn.com/")
If the first function gets stuck for some reason, the second and third function will not be executed and will be waiting for the first one to finish.
Now, take a look at the code below:
go getUrlContents("https://www.startpage.com/")
go getUrlContents("https://protonmail.com/")
go getUrlContents("https://nordvpn.com/")
By simply introducing the go
keyword, you will be creating 3 Go Routines (one for each function call). If you do not have a multicore computer, these functions will be executing concurrently, which means that if the first one gets stuck (waiting for a response from www.startpage.com
), go will start executing the second one.
For this challenge, your goal is to spin up multiple Go routines when saving secrets to Vault. Obviously, you will not notice any difference in execution time becase it's a very simple example, but if you're executing a program that does a lot of heavy processing, Go Routines can make a difference.
Since this is the last part of the Dojo, it only makes sense to be the most challenging one as well :)
Before you start racking your brain... :) Here are some tips:
- As you will probably notice, simply using the
go
keyword in front of function will not be enough - Use channels to help with the communication between Go Routines (see section below for a list of links that explain what channels are)
- In
main()
, after you make 2 calls toSaveSecret()
, you should read twice from the channel you created.
Good luck with Phase 7 and don't hesitate to contact one of the organizers should you have any questions!
https://tour.golang.org/concurrency/2
https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb
https://www.sohamkamani.com/blog/2017/08/24/golang-channels-explained/
https://tutorialedge.net/golang/go-channels-tutorial/
https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8
The Definition of Done is the same as Phase 6.