d

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.

15 St Margarets, NY 10033
(+381) 11 123 4567
ouroffice@aware.com

 

KMF

How To Create a Restful Client in Go?

REST is an acronym for REpresentational State Transfer and is primarily an architecture for designing web services. Although web services exchange information in HTML, RESTful services usually use JSON format, which is well supported by Go. REST is not tied to any operating system or system architecture and is not a protocol; however, to implement a RESTful service, you need to use a protocol such as HTTP. When developing RESTful servers, you need to create the appropriate Go structures and perform the necessary marshaling and unmarshaling operations for the exchange of JSON data. 

This article is part of a chapter from the¬†third edition¬†of my book¬†Mastering Go¬†‚Äď a book that helps you master key features of Go, including advanced concepts like concurrency and working with JSON, to create and optimize real-world services, network servers, and clients.¬†

Creating a RESTful Client 

Creating a RESTful client is much easier than programming a server mainly because you do not have to work with the database on the client-side. The only thing that the client needs to do is send the right amount and kind of data to the server and receive back the server response. The RESTful client is going to be developed in¬†~/go/src/github.com/mactsouk/rest-cli‚ÄĒif you do not plan to make it available to the world, you do not need to create a separate GitHub repository for it.¬†

For you to be able to see the code of the client, I created a GitHub repository. 

The supported first-level cobra commands are the following: 

  • list: This command accesses the¬†/getall¬†endpoint and returns the list of users.¬†
  • time: This command is for visiting the¬†/time¬†endpoint.¬†
  • update: This command is for updating user records‚ÄĒthe user ID cannot change.¬†
  • logged: This command lists all logged-in users.¬†
  • delete: This command deletes an existing user.¬†
  • login: This command is for logging in a user.¬†
  • logout: This command is for logging out a user.¬†
  • add: This command is for adding a new user to the system.¬†
  • getid: This command returns the ID of a user, identified by their username.¬†
  • search: This command displays information about a given user, identified by their ID.¬†

A client like the one we are about to present is much better than working with curl(1) because it can process the received information but, most importantly, it can interpret the HTTP return codes and preprocess data before sending it to the server. The price you pay is the extra time needed for developing and debugging the RESTful client. 

There exist two main command-line flags for passing the username and the password of the user issuing the command:¬†user¬†and¬†pass. As you are going to see in their implementations, they have the¬†-u¬†and¬†-p¬†shortcuts, respectively. Additionally, as the JSON record that holds user info has a small number of fields, all the fields are going to be given in a JSON record as plain text, using the¬†data¬†flag and¬†-d¬†shortcut‚ÄĒthis is implemented in¬†root.go.¬†

Each command is going to read the desired flags only and the desired fields of the input JSON record‚ÄĒthis is implemented in the source code file of each command. Last, the utility is going to return JSON records back, when this makes sense or a text message related to the endpoint that was visited. Now, let us continue with the structure of the client and the implementation of the commands.¬†

Creating the Structure of the Command-line Client

This subsection uses the cobra utility to create a structure for the command-line utility. But first, we are going to create a proper cobra project and Go module: 

$ cd ~/go/src/github.com/mactsouk 
$ git clone git@github.com:mactsouk/rest-cli.git 
$ cd rest-cli 
$ ~/go/bin/cobra init --pkg-name github.com/mactsouk/rest-cli 
$ go mod init 
$ go mod tidy 
$ go run main.go 

You do not need to execute the last command, but it makes sure that everything is fine so far. After that, we are ready to define the commands that the utility is going to support by running the next cobra commands: 

$ ~/go/bin/cobra add add 
$ ~/go/bin/cobra add delete 
$ ~/go/bin/cobra add list 
$ ~/go/bin/cobra add logged 
$ ~/go/bin/cobra add login 
$ ~/go/bin/cobra add logout 
$ ~/go/bin/cobra add search 
$ ~/go/bin/cobra add getid 
$ ~/go/bin/cobra add time 
$ ~/go/bin/cobra add update 

Now that we have the desired structure, we can begin implementing the commands and maybe remove some of the comments inserted by the cobra, which is the subject of the next subsection. 

Implementing RESTful Client Commands 

As there is no point in presenting all code that can be found in the GitHub repository, we are going to present the most characteristic code as found in some of the commands, starting with root.go, which is where the next global variables are defined: 

var SERVER string 
var PORT string 
var data string 
var username string 
var password string 

These global variables hold the values of the command-line options of the utility and are accessible from anywhere in the utility code. 

type User struct { 
ID        int    `json:"id"` 
Username  string `json:"username"` 
Password  string `json:"password"` 
LastLogin int64  `json:"lastlogin"` 
Admin     int    `json:"admin"` 
Active    int    `json:"active"` 
} 

We define the User structure for sending and receiving data. 

func init() { 
rootCmd.PersistentFlags().StringVarP(&username, "username", "u", "username", "The username") 
rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "admin", "The password") 
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "{}", "JSON Record") 
 
rootCmd.PersistentFlags().StringVarP(&SERVER, "server", "s", "http://localhost", "RESTful server hostname") 
rootCmd.PersistentFlags().StringVarP(&PORT, "port", "P", ":1234", "Port of RESTful Server") 
} 

Last, we present the implementation of the init() function that holds the definitions of the command-line options. The values of the command-line flags are automatically stored in the variables that are passed as the first argument to rootCmd.PersistentFlags().StringVarP(). So, the username flag, which has the -u alias, stores its value to the username global variable. 

Next is the implementation of the list command as found in list.go: 

var listCmd = &cobra.Command{ 
Use:   "list", 
Short: "List all available users", 
Long:  `The list command lists all available users.`, 

This part is about the help messages that are displayed for the command‚ÄĒalthough they are optional, it is good to have an accurate description of the command. We continue with the implementation:¬†

Run: func(cmd *cobra.Command, args []string) { 
endpoint := "/getall" 
user := User{Username: username, Password: password} 

First, we construct a¬†User¬†variable to hold the username and the password of the user issuing the command‚ÄĒthis variable is going to be passed to the server.¬†

// bytes.Buffer is both a Reader and a Writer 
buf := new(bytes.Buffer) 
err := user.ToJSON(buf) 
if err != nil { 
fmt.Println("JSON:", err) 
return 
} 

We need to encode the¬†user¬†variable before passing it to the RESTful server, which is the purpose of the¬†ToJSON()¬†method‚ÄĒthe implementation of the¬†ToJSON()¬†method is found in¬†root.go.¬†

req, err := http.NewRequest(http.MethodGet, 
                            SERVER+PORT+endpoint, buf) 
if err != nil { 
fmt.Println("GetAll ‚Äď Error in req: ", err)¬†
return 
} 
req.Header.Set("Content-Type", "application/json") 

Here, we create the request using the SERVER and PORT global variables followed by the endpoint, using the desired HTTP method (http.MethodGet), and declare that we are going to send JSON data using Header.Set(). 

c := &http.Client{ 
Timeout: 15 * time.Second, 
} 
 
resp, err := c.Do(req) 
if err != nil { 
fmt.Println("Do:", err) 
return 
} 

After that, we send our data to the server using Do() and get the server response. 

if resp.StatusCode != http.StatusOK { 
fmt.Println(resp) 
return 
} 

If the status code of the response is not http.StatusOK, then the request has failed. 

var users = []User{} 
SliceFromJSON(&users, resp.Body) 
data, err := PrettyJSON(users) 
if err != nil { 
fmt.Println(err) 
return 
} 
 
fmt.Print(data) 
}, 
} 

If the status code is http.StatusOK, then we prepare to read a slice of User variables. As these variables hold JSON records, we need to decode them using SliceFromJSON(), which is defined in root.go. 

Last is the code of the¬†add¬†command, as found in¬†add.go‚ÄĒthe difference between¬†add¬†and¬†list¬†is that the¬†add¬†the command needs to send two JSON records to the RESTful server: the first one holding the data of the user issuing the command and the second holding the data for the user that is about to be added to the system. The¬†username¬†and¬†password¬†flags hold the data for the¬†Username¬†and¬†Password¬†fields of the first record whereas the¬†data¬†command-line flag holds the data for the second record.¬†

var addCmd = &cobra.Command{ 
Use:   "add", 
Short: "Add a new user", 
Long:  `Add a new user to the system.`, 
Run: func(cmd *cobra.Command, args []string) { 
fmt.Println("add called") 
endpoint := "/add" 
u1 := User{Username: username, Password: password} 

As before, we get the information about the user issuing the command that put it into a structure. 

// Convert data string to User Structure 
var u2 User 
err := json.Unmarshal([]byte(data), &u2) 
if err != nil { 
fmt.Println("Unmarshal:", err) 
return 
} 

As the¬†data¬†command-line flag holds a string value, we need to convert that string value to a¬†User¬†structure‚ÄĒthis is the purpose of the¬†json.Unmarshal()¬†call.¬†

users := []User{} 
users = append(users, u1) 
users = append(users, u2) 

Then we create a slice of¬†User¬†variables that are going to be sent to the server‚ÄĒthe order you put the structures in that slice is important: first, the user issuing the command and then the data of the user that is going to be created.¬†¬†

buf := new(bytes.Buffer) 
err = SliceToJSON(users, buf) 
if err != nil { 
fmt.Println("JSON:", err) 
return 
} 

Then we encode that slice before sending it to the RESTful server through the HTTP request. 

req, err := http.NewRequest(http.MethodPost, 
                                   SERVER+PORT+endpoint, buf) 
if err != nil { 
fmt.Println("GetAll¬†‚Äď Error in¬†req: ", err)¬†
return 
} 
req.Header.Set("Content-Type", "application/json") 
 
c := &http.Client{ 
Timeout: 15 * time.Second, 
} 
 
resp, err := c.Do(req) 
if err != nil { 
fmt.Println("Do:", err) 
return 
} 

We prepare the request and send it to the server‚ÄĒthe server is responsible for decoding the provided data and acting accordingly, in this case adding a new user to the system. The client just needs to visit the correct endpoint using the appropriate HTTP method (http.MethodPost) and check the returned status code.¬†

if resp.StatusCode != http.StatusOK { 
fmt.Println("Status code:", resp.Status) 
} else { 
fmt.Println("User", u2.Username, "added.") 
} 
}, 
} 

The¬†add¬†command does not return any data back to the client‚ÄĒwhat interests us in the HTTP status code because this is what determines the success or the failure of the command.¬†

Using RESTful Client 

We are now going to use the command-line utility to interact with the RESTful server‚ÄĒthis type of utility can be used for administering a RESTful server, creating automated tasks, and carrying out CI/CD jobs. For reasons of simplicity the client and the server reside on the same machine, and we mostly work with the default user (admin)¬†‚ÄĒthis makes the presented commands shorter. Additionally, we executed¬†go build¬†to create a binary executable to avoid using¬†go main.go¬†all the time.¬†

First, we get the time from the server: 

$ ./rest-cli time 
The current time is: Tue, 25 May 2021 08:38:04 EEST 

Next, we list all users‚ÄĒas the output depends on the contents of the database, we print a small part of the output. Note that the¬†list¬†command requires a user with admin privileges:¬†

$ ./rest-cli list -u admin -p admin 
[ 
{ 
"id": 7, 
"username": "mike", 
"password": "admin", 
"lastlogin": 1620926862, 
"admin": 1, 
"active": 0 
}, 

Next, we test the logged command with an invalid password: 

$ ./rest-cli logged -u admin -p notPass 
&{400 Bad Request 400 HTTP/1.1 1 1 map[Content-Length:[0] Date:[Tue, 25 May 2021 05:42:36 GMT]] 0xc000190020 0 [] false false map[] 0xc0000fc800 <nil>} 

As expected, the command fails‚ÄĒthis output is used for debugging purposes. After making sure that the command works as expected, you might want to print a more appropriate error message.¬†

After that, we test the add command: 

$ ./rest-cli add -u admin -p admin --data '{"Username":"newUser", "Password":"aPass"}' 
User newUser added. 

Trying to add the same user again is going to fail: 

$ ./rest-cli add -u admin -p admin --data '{"Username":"newUser", "Password":"aPass"}' 
Status code: 400 Bad Request 

Next, we are going to delete¬†newUser‚ÄĒbut first, we need to find the user ID of¬†newUser:¬†

$ ./rest-cli getid -u admin -p admin --data '{"Username":"newUser"}' 
User newUser has ID: 15 
$ ./rest-cli delete -u admin -p admin --data '{"ID":15}' 
User with ID 15 deleted. 

Feel free to continue testing the RESTful client and let me know if you find any bugs! 

Summary 

Go is widely used for developing RESTful clients and servers and this article illustrated how to program professional RESTful clients in Go. You also learned how to use a commandline utility to interact with RESTful servers.   

Credit: Source link

Previous Next
Close
Test Caption
Test Description goes like this