Skip to content
This repository was archived by the owner on Jun 23, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions exercise_answers/nacchan/http_exercise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
)

type UserAttributes struct {
Id int `json:"id"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick about naming; in Go we need to use consistent case (all small, or all capitalized) for initialisms and acronyms. For example, ID, AccountIDs, id (for unexported variable), userID etc.

You can see Go Wiki and the Google Style Guide for more examples!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google style guide looks very easy to understand! Thanks👏

Name string `json:"name"`
AccountIds []int `json:"account_ids"`
}

type AccountAttributes struct {
Id int `json:"id"`
UserId int `json:"user_id"`
Name string `json:"name"`
Balance int `json:"balance"`
}

type UserResponse struct {
UserAttributes UserAttributes `json:"attributes"`
}

type AccountResponse struct {
AccountAttributes AccountAttributes `json:"attributes"`
}

const URL = "https://sample-accounts-api.herokuapp.com/users/"

func main() {
var userId int
reader := bufio.NewReader(os.Stdin)
for {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice loop!

There are many ways to read input in Go, including bufio, fmt.Scanf, and others. I am not sure which one is the most performant or most common, but your solution seems robust!

fmt.Println("Enter user id: ")
userIdString, err := reader.ReadString('\n')
userIdString = strings.TrimSpace(userIdString)
userId, err = strconv.Atoi(userIdString)
if err != nil {
fmt.Println("please enter a valid integer")
continue
}
break
}

user, err := getUser(userId)
if err != nil {
fmt.Println("Error getting user: ", err)
return
}

accounts, err := getAccounts(userId)
if err != nil {
fmt.Println("Error getting accounts: ", err)
return
}

fmt.Println("User: ", user)
fmt.Println("Accounts: ")
printAccounts(accounts)
}

func getUser(id int) (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great solution!

resp, err := http.Get(URL + strconv.Itoa(id))

if err != nil {
return "", fmt.Errorf("error in making request: %v", err)
Copy link
Collaborator

@mavrikis-kostas mavrikis-kostas Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message looks great! It follows the error strings suggestion of using not-capitalized strings, which is correct. There is only one thing that can be improved here, which is using %w instead of %v for wrapping the errors.

The %w verb creates "wrapped" (nested) errors which can be unwrapped using the errors package. You can check the errors package documentation for more info!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, by wrapping you can use error.Is later to check it holds specific error or not👀 I need to understand about go error handling more, thanks for letting me know!

}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("response error: %v", resp.StatusCode)
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error in reading response body: %v", err)
}

var res UserResponse
err = json.Unmarshal(body, &res)

if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes down to personal preferences, so other people may have different suggestions on this. But a more idiomatic way of retrieving an error and checking if it's nil, is like this:

if err = json.Unmarshal(body, &res); err != nil {
	return "", fmt.Errorf("error in parsing json: %v", err)
}

The main benefit of this approach is that the scope of the err variable is limited to the if statement. This style is used whenever the function that you want to check (in this case, json.Unmarshal()) only returns an error object, and you want to immediately check the error.


Note: your solution is also perfectly valid, since the err variable has already been declared in a previous statement. The code that I recommended does not actually limit the scope of the outer err variable, but it's still considered more "go idiomatic", aka it's more common.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning variables at the beginning of an if statement certainly gives it a go-like style!(I never saw this style in other language🧐) While I understand that limiting the variable scope isn't always essential, I agree it’s considered good practice and I'll keep that in mind!

return "", fmt.Errorf("error in parsing json: %v", err)
}

return res.UserAttributes.Name, nil
}

func getAccounts(id int) ([]AccountAttributes, error) {
resp, err := http.Get(URL + strconv.Itoa(id) + "/accounts")

if err != nil {
return nil, fmt.Errorf("error in making request: %v", err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("response error: %v", resp.StatusCode)
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error in reading response body: %v", err)
}

var res []AccountResponse
err = json.Unmarshal(body, &res)
if err != nil {
return nil, fmt.Errorf("error in parsing json: %v", err)
}

var accounts []AccountAttributes = []AccountAttributes{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many ways to initialize slices in Go, such as:

  • var accounts []AccountAttributes = []AccountAttributes{}
    • creates a slice with 0 length/capacity, appending elements will resize the slice
    • this approach is generally not recommended in Go (instead, use the next approach)
  • var accounts []AccountAttributes
    • creates a nil slice with 0 length/capacity, appending elements will resize the slice
    • this approach is recommended if you do not know (or cannot estimate) the final size of the slice
  • accounts := make([]AccountAttributes, len(res))
    • creates a slice with 0 length, but preconfigured capacity, appending elements will only resize the slice if the capacity is not enough
    • this approach is recommended when you already know (or can estimate) the final size of the slice

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I haven't noticed that there's such difference between []type and []type{}, but I noticed that Intellij blamed me when I have []type {} code! 👀
スクリーンショット 2025-04-24 15 42 51


for _, account := range res {
accounts = append(accounts, account.AccountAttributes)
}

return accounts, nil
}

func printAccounts(accounts []AccountAttributes) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice function!

var totalBalance int = 0

for _, account := range accounts {
fmt.Println(" -", account.Name, ": ", account.Balance)
totalBalance += account.Balance
}
fmt.Println("Total balance: ", totalBalance)
}
17 changes: 17 additions & 0 deletions exercise_answers/nacchan/validator/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package validator

// IsInRange checks if a number is within the specified range (inclusive)
func IsInRange(num, min, max int) bool {
// Check if the number is less than the minimum
if num < min {
return false
}

// Check if the number is greater than the maximum
if num > max {
return false
}

// If we get here, the number is within range
return true
}
65 changes: 65 additions & 0 deletions exercise_answers/nacchan/validator/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package validator

import (
"testing"
)

func TestIsInRange(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great solution!!

tt := []struct {
name string
num, min, max int
Copy link
Collaborator

@mavrikis-kostas mavrikis-kostas Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your approach is common to separate the input and output parameters of the test! In this case, num, min, max are all input parameters, so they are placed in the same line 👏

Another way to separate them, if we have a lot of input parameters (maybe 4+), is using an extra struct. Check the IntelliJ-generated code that I pasted in the below comment.

expected bool
}{
{
name: "within range",
num: 5,
min: 1,
max: 10,
expected: true,
},
{
name: "below range",
num: 0,
min: 1,
max: 10,
expected: false,
},
{
name: "above range",
num: 11,
min: 1,
max: 10,
expected: false,
},
{
name: "equal to min",
num: 1,
min: 1,
max: 10,
expected: true,
},
{
name: "equal to max",
num: 10,
min: 1,
max: 10,
expected: true,
},
{
name: "zero",
num: 0,
min: 0,
max: 0,
expected: true,
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
result := IsInRange(tc.num, tc.min, tc.max)
if result != tc.expected {
t.Errorf("expected %v, got %v", tc.expected, result)
Copy link
Collaborator

@mavrikis-kostas mavrikis-kostas Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is clear, and shows what went wrong in the test!

But some small optional adjustments are:

  • Instead of expected x, got y, Go usually follows the opposite order got x, want y. This is also explained briefly in the "Useful Test Failures" section of the Go Wiki.
  • Mentioning the function name is also a common pattern, so in this case the error would be:
    • t.Errorf("IsInRange() = %v, want %v", result, tt.expected)

Note: in IAA we will probably use the assert package of the testify library, so the error messages will automatically be handled by the library.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this is the auto-generated test from IntelliJ, that follows the general Go conventions:

func TestIsInRange(t *testing.T) {
	type args struct {
		num int
		min int
		max int
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := IsInRange(tt.args.num, tt.args.min, tt.args.max); got != tt.want {
				t.Errorf("IsInRange() = %v, want %v", got, tt.want)
			}
		})
	}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see—it's a Go convention to include the test's objective in the function name and clearly distinguish between got and want. Thanks for pointing out this helpful reference!

}
})
}
}