Skip to content

Commit

Permalink
Review Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
PragatiVerma18 committed Jan 10, 2025
1 parent 5df1f18 commit 788a453
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 60 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ yarn-error.log*
*.swp
*.swo
tmp
pnpm-lock.yaml
205 changes: 146 additions & 59 deletions content/go/concepts/working-with-json/working-with-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,43 @@ Subjects:
- 'Computer Science'
Tags:
- 'JSON'
- 'Marshal'
- 'Unmarshal'
- 'Encode'
- 'Decode'
- 'Data'
- 'Development'
- 'Objects'
CatalogContent:
- 'learn-go'
- 'paths/back-end-engineer-career-path'
---

**JSON** is one of the most commonly used data exchange formats for communication between web applications, other services, mobile applications, etc.

Go provides built-in functions to work with JSON effectively. Below are some of the most commonly used functions:
Go provides built-in functions to work with JSON effectively.

| Feature | `json.Marshal` | `json.NewDecoder` |
| ------------ | ----------------------------------------------------------------------------- | ------------------------------------------------ |
| Input | Go value (`struct`, `map`, etc.) | `io.ReadCloser` (network connection) |
| Output | JSON byte slice | Go value (struct, map, etc.) |
| Memory Usage | Can potentially use more memory if the input data is large | Generally more memory-efficient for large inputs |
| Use Cases | Converting Go data to JSON for various purposes, suitable for small data sets | Decoding JSON data from streams or large files |
## Using Marshal and Unmarshal

## Marshal and Unmarshal
In Go, `json.Marshal` encodes a Go data structure into a JSON-formatted string. This encoded data can then be saved to a file, transmitted, or used in other applications.

The syntax of `json.Marshal` looks like this:

In Go, `json.Marshal` encodes a Go data structure into a JSON-formatted string. This encoded data can then be saved to a file, transmitted, or used in other applications.
```pseudo
data, err := json.Marshal(v)
```

- `v`: The Go value to be marshaled (e.g., `struct`, `map`).
- `data`: The result of the marshaling, returned as a byte slice (`[]byte`).
- `err`: An error, if any, that occurred during marshaling.

Conversely, `json.Unmarshal` decodes JSON data into a Go data structure. This allows for operations such as adding new elements, extracting specific values (e.g., configurations), or modifying existing ones, as demonstrated in the example below:
Conversely, `json.Unmarshal` decodes JSON data into a Go data structure. The syntax for `json.Unmarshal` looks like this:

```pseudo
err := json.Unmarshal(data, &v)
```

- `data`: The JSON byte slice to be unmarshaled.
- `v`: The Go value (typically a pointer to a `struct` or `map`) where the unmarshaled data will be stored.
- `err`: An error, if any, that occurred during unmarshaling.

This allows for operations such as adding new elements, extracting specific values (e.g., configurations), or modifying existing ones, as demonstrated in the example below:

```go
package main
Expand All @@ -43,7 +54,7 @@ import (
"strings"
)

// We need to define a struct that mirrors the structure of the JSON data we want to unmarshal.
// Define a struct that mirrors the structure of the JSON data we want to unmarshal.
type Meal struct {
Name string `json:"name"`
Ingredients []string `json:"ingredients"`
Expand All @@ -64,63 +75,127 @@ func main() {
]
}`

// We need a variable to store the data decoded from the unmarshaling process.
// Declare a variable to store the decoded data.
meals := Meals{}

// To unmarshal the data, we need to provide:
// 1. Data in the format of a slice of bits.
// 2. A pointer to the location where the data will be stored.
// Unmarshal the JSON string into the `meals` struct.
if err := json.Unmarshal([]byte(menu), &meals); err != nil {
// Handle the errors in case they occur.
fmt.Println(err)
fmt.Println("Error unmarshalling data:", err)
return
}

// In case we need to add a new object to the
// Add a new meal to the menu.
meal := Meal{
Name: "Pupusas",
Ingredients: []string{"Cheese", "Refried beans", "Tomato Sauce", "Rice flour", "Pickled cabbage"},
}

// Add new meal for the menu
// Add the new meal to the list.
meals.Meals = append(meals.Meals, meal)

// Add new meal in line for the menu
// Add another meal inline.
meals.Meals = append(meals.Meals, Meal{Name: "Sushi", Ingredients: []string{"Rice", "Fish", "Seaweed", "Wasabi", "Soy Sauce", "Ginger"}})

// List the menu of meals
// List the menu of meals.
for _, meal := range meals.Meals {
fmt.Printf("---\nMeal: %s\ningredients: %s \n", meal.Name, strings.Join(meal.Ingredients, ", "))
fmt.Printf("---\nMeal: %s\nIngredients: %s\n", meal.Name, strings.Join(meal.Ingredients, ", "))
}

// Serialize the object to a slice of bytes
// Serialize the object to a slice of bytes.
data, err := json.Marshal(meals)
if err != nil {
fmt.Println(err)
fmt.Println("Error marshalling data:", err)
return
}

fmt.Println("-------------")
fmt.Println(string(data))

// Crate an empty file
// Create an empty file.
file, err := os.Create("meals.json")
if err != nil {
fmt.Println("Error opening file:", err)
fmt.Println("Error creating file:", err)
return
}

// Ensure the file is closed after the program finishes.
defer file.Close()

// Write data into the file previously created
// Write the serialized data into the file.
if _, err = file.Write(data); err != nil {
fmt.Println(err)
fmt.Println("Error writing to file:", err)
return
}

fmt.Println("Data has been written to meals.json successfully.")
}
```

When you run the above Go program, the following will happen:

- The menu of meals will be printed to the console.
- The serialized JSON data will be printed to the console.
- A `meals.json` file will be created in the working directory with the serialized data.

Here's how the console output will look like:

```shell
---
Meal: Pizza Margherita
Ingredients: Dough, Tomato Sauce, Mozzarella Cheese, Tomato, Basil
---
Meal: Spaghetti Carbonara
Ingredients: Spaghetti, Eggs, Pancetta, Pecorino Romano Cheese, Black Pepper
---
Meal: Chicken Stir-fry
Ingredients: Chicken, Vegetables (e.g., Broccoli, Carrots, Onions, Peppers), Rice, Soy Sauce, Ginger, Garlic
---
Meal: Tacos al Pastor
Ingredients: Pork, Pineapple, Tortillas, Onion, Cilantro
---
Meal: Caesar Salad
Ingredients: Romaine Lettuce, Croutons, Parmesan Cheese, Caesar Dressing
---
Meal: Pupusas
Ingredients: Cheese, Refried beans, Tomato Sauce, Rice flour, Pickled cabbage
---
Meal: Sushi
Ingredients: Rice, Fish, Seaweed, Wasabi, Soy Sauce, Ginger
-------------
{"meals":[{"name":"Pizza Margherita","ingredients":["Dough","Tomato Sauce","Mozzarella Cheese","Tomato","Basil"]},{"name":"Spaghetti Carbonara","ingredients":["Spaghetti","Eggs","Pancetta","Pecorino Romano Cheese","Black Pepper"]},{"name":"Chicken Stir-fry","ingredients":["Chicken","Vegetables (e.g., Broccoli, Carrots, Onions, Peppers)","Rice","Soy Sauce","Ginger","Garlic"]},{"name":"Tacos al Pastor","ingredients":["Pork","Pineapple","Tortillas","Onion","Cilantro"]},{"name":"Caesar Salad","ingredients":["Romaine Lettuce","Croutons","Parmesan Cheese","Caesar Dressing"]},{"name":"Pupusas","ingredients":["Cheese","Refried beans","Tomato Sauce","Rice flour","Pickled cabbage"]},{"name":"Sushi","ingredients":["Rice","Fish","Seaweed","Wasabi","Soy Sauce","Ginger"]}]
}
Data has been written to meals.json successfully.
```

## NewEncoder and NewDecoder
## Using NewEncoder and NewDecoder

In Go, `json.NewEncoder` is commonly used to encode data into a JSON format, making it suitable for exchanging information between the backend and external clients that require a JSON response.

The syntax for `json.NewEncoder` looks like this:

```pseudo
encoder := json.NewEncoder(w)
err := encoder.Encode(v)
```

- `w`: The `io.Writer` (e.g., `http.ResponseWriter`, file, etc.) where the JSON output will be written.
- `v`: The Go value (typically a `struct`, `map`, etc.) to be encoded into JSON.
- `encoder`: A new `json.Encoder` instance that is used to encode the Go value.
- `err`: An error, if any, that occurred during encoding.

On the other hand, `json.NewDecoder` is used to decode data received from a REST API, transforming it into a usable Go data structure.

In Go `json.NewDecoder` is used to format data received from a REST API. Other function that is commonly used is `json.NewEncoder` to exchange data between backend side and external clients that require a JSON response.
The syntax for `json.NewDecoder` looks like this:

```pseudo
decoder := json.NewDecoder(r)
err := decoder.Decode(v)
```

- `r`: The `io.Reader` (e.g., `http.Request.Body`, file) containing the JSON data to be decoded.
- `v`: A pointer to the Go value (typically a `struct`, `map`, etc.) where the decoded data will be stored.
- `decoder`: A new `json.Decoder` instance that is used to decode the JSON data.
- `err`: An error, if any, that occurred during decoding.

In the following example, we will demonstrate the usage of both functions. We will make a call to an external REST API and enhance the result by adding products made from the fruit specified in the query parameter. For example: `http://localhost:4444/fruit?name=Strawberry`

Expand Down Expand Up @@ -150,57 +225,52 @@ type Fruit struct {

func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /fruit", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/fruit", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// Get query params from URL
params := r.URL.Query()

// We are going to validate the name of the fruit
// Validate the name of the fruit
if fruitName := params.Get("name"); len(fruitName) > 0 {

// Set the specific content type for the response
// Set the content type for the response
w.Header().Set("Content-Type", "application/json")

// Get the results of the invocation to the REST API.
// Get the results of the invocation to the REST API
result, err := GetFruitData(fruitName)

// We need to handle any errors that occur during the decoding process.
if err != nil {
http.Error(w, "Error fruit data not found", http.StatusNotFound)
http.Error(w, fmt.Sprintf("Failed to get fruit data: %v", err), http.StatusInternalServerError)
return
}

// We need a variable to store the results from the decoding process.
// Decode the response into the fruit struct
fruit := Fruit{}

// To Decode the data, we need to provide:
// 1. Data in the format of io.ReadCloser.
// 2. A pointer to the location where the data will be stored.
err = json.NewDecoder(result).Decode(&fruit)

// We need to handle any errors that occur during the decoding process.
if err != nil {
http.Error(w, "Error on decoding data", http.StatusInternalServerError)
http.Error(w, "Error decoding data", http.StatusInternalServerError)
return
}

// We need a map of products, keyed by fruit, to enhance the results of the external REST API.
// Enhance the fruit data with products
productsByFruit := ProductsByFruit{
"strawberry": {"Smoothies", "Ice cream", "Jelly"},
"banana": {"Banana split", "Smoothies"},
"tomato": {"Salads", "Sauces"},
"raspberry": {"Pies", "Smoothies"},
"orange": {"Juice", "Jelly"},
"blueberry": {"Smoothies", "Pies"},
"pumpkin": {"Pies", "Late"},
"pumpkin": {"Pies", "Latte"},
}

// Set the products by fruit
fruit.Products = productsByFruit[strings.ToLower(fruitName)]

// Other way to handle any errors.
// Encode the modified fruit struct to the response
if err := json.NewEncoder(w).Encode(fruit); err != nil {
http.Error(w, "Error on encoding data", http.StatusInternalServerError)
http.Error(w, "Error encoding data", http.StatusInternalServerError)
return
}

Expand All @@ -216,17 +286,34 @@ func main() {
}

func GetFruitData(name string) (io.ReadCloser, error) {
// Retrieve data form external REST API.
// Retrieve data from external REST API
response, err := http.Get(fmt.Sprintf("https://www.fruityvice.com/api/fruit/%s", name))
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
if response == nil {
return nil, errors.New("no response from the server")
}

// Handle errors
// Handle different HTTP status codes
switch response.StatusCode {
case http.StatusOK:
return response.Body, err
return response.Body, nil
case http.StatusNotFound:
return nil, errors.New("is the fruit name you entered correct")
return nil, errors.New("fruit not found")
default:
return nil, fmt.Errorf("errors on the server with status: %d", response.StatusCode)
return nil, fmt.Errorf("server error with status: %d", response.StatusCode)
}
}
```

## Comparison between `json.Marshal` and `json.NewDecoder`

Below is a comparison between `json.Marshal` and `json.NewDecoder` based on their use cases and performance considerations:

| Feature | `json.Marshal` | `json.NewDecoder` |
| ------------ | ----------------------------------------------------------------------------- | ------------------------------------------------ |
| Input | Go value (`struct`, `map`, etc.) | `io.ReadCloser` (network connection) |
| Output | JSON byte slice | Go value (struct, map, etc.) |
| Memory Usage | Can potentially use more memory if the input data is large | Generally more memory-efficient for large inputs |
| Use Cases | Converting Go data to JSON for various purposes, suitable for small data sets | Decoding JSON data from streams or large files |

0 comments on commit 788a453

Please sign in to comment.