Welcome to our lesson on mastering data aggregation and data streams with Go. In this lesson, you'll learn to build a basic sales records aggregator using Go's standard library and map data structures. We'll extend this functionality to handle more advanced operations such as filtering, data aggregation, and formatting. By the end of this lesson, you'll be proficient in managing and formatting data streams efficiently in Go.
To get started, we'll create a simple sales record aggregator in Go. Here are the functions we'll focus on:
-
func (s *SalesAggregator) AddSale(saleID string, amount float64, date time.Time)
- Adds or updates a sale record with a unique identifiersaleID
,amount
, anddate
. -
func (s *SalesAggregator) GetSale(saleID string) (float64, bool)
- Retrieves the sale amount associated with thesaleID
. Returns the amount and abool
indicating if the sale exists. -
func (s *SalesAggregator) DeleteSale(saleID string) bool
- Deletes the sale record with the givensaleID
. Returnstrue
if the sale was deleted,false
if it does not exist.
Are these functions clear so far? Great! Let's now look at how we would implement them.
Here is the complete code for the starter task:
Go1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8type sale struct { 9 amount float64 10 date time.Time 11} 12 13type SalesAggregator struct { 14 sales map[string]sale 15} 16 17func (s *SalesAggregator) AddSale(saleID string, amount float64, date time.Time) { 18 s.sales[saleID] = sale{amount: amount, date: date} 19} 20 21func (s *SalesAggregator) GetSale(saleID string) (float64, bool) { 22 sale, exists := s.sales[saleID] 23 return sale.amount, exists 24} 25 26func (s *SalesAggregator) DeleteSale(saleID string) bool { 27 if _, exists := s.sales[saleID]; exists { 28 delete(s.sales, saleID) 29 return true 30 } 31 return false 32} 33 34func main() { 35 aggregator := SalesAggregator{sales: make(map[string]sale)} 36 37 // Add sales 38 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 39 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 40 41 // Get sale 42 if amount, found := aggregator.GetSale("001"); found { 43 fmt.Println(amount) // Output: 100.5 44 } else { 45 fmt.Println("Sale not found") 46 } 47 48 // Delete sale 49 fmt.Println(aggregator.DeleteSale("002")) // Output: true 50 if amount, found := aggregator.GetSale("002"); found { 51 fmt.Println(amount) 52 } else { 53 fmt.Println("Sale not found") // Output: Sale not found 54 } 55}
Let's quickly discuss this starter setup:
- The
sales
map stores sale records withsaleID
as the key and asale
struct containingamount
anddate
as the value. AddSale
adds a new sale or updates an existing sale ID.GetSale
retrieves the amount for a given sale ID and returns abool
to indicate if the sale exists.DeleteSale
removes the sale record for the given sale ID and returns abool
to indicate success.
Now that we have our basic aggregator, let's extend it to include more advanced functionalities.
To add complexity and usefulness to our sales aggregator, we'll introduce some additional functions for advanced data aggregation, filtering, and formatting functionalities.
-
func (s *SalesAggregator) AggregateSales(minAmount float64) (int, float64)
- Returns the total number of sales and the total sales amount where the sale amount is aboveminAmount
. -
func (s *SalesAggregator) FormatSales(minAmount float64) string
- Returns the sales data, filtered byminAmount
, formatted as a plain text string. Includes sales statistics in the output. -
func (s *SalesAggregator) GetSalesInDateRange(startDate, endDate time.Time) []SaleDetail
- Retrieves all sales that occurred within the given date range, inclusive. Each sale includessaleID
,amount
, anddate
.
Let's implement these functions step by step.
We start by creating the AggregateSales
function:
Go1// ... previous code 2func (s *SalesAggregator) AggregateSales(minAmount float64) (int, float64) { 3 totalSales := 0 4 totalAmount := 0.0 5 for _, sale := range s.sales { 6 if sale.amount > minAmount { 7 totalSales++ 8 totalAmount += sale.amount 9 } 10 } 11 return totalSales, totalAmount 12} 13 14func main() { 15 aggregator := SalesAggregator{sales: make(map[string]sale)} 16 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 17 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 18 19 totalSales, totalAmount := aggregator.AggregateSales(50) 20 fmt.Printf("Total Sales: %d, Total Amount: %.2f\n", totalSales, totalAmount) 21 // Output: Total Sales: 2, Total Amount: 301.25 22}
This method simply iterates through the sales and sums those that exceed the minAmount
.
Next, we create the FormatSales
function to output data in plain text format.
Go1// ... previous code 2func (s *SalesAggregator) FormatSales(minAmount float64) string { 3 var builder strings.Builder 4 totalSales, totalAmount := s.AggregateSales(minAmount) 5 6 builder.WriteString("Sales:\n") 7 for saleID, sale := range s.sales { 8 if sale.amount > minAmount { 9 builder.WriteString(fmt.Sprintf("Sale ID: %s, Amount: %.2f, Date: %s\n", saleID, sale.amount, sale.date.Format("2006-01-02"))) 10 // date.Format("2006-01-02") formats date as "YYYY-MM-DD" 11 } 12 } 13 14 builder.WriteString(fmt.Sprintf("Summary:\nTotal Sales: %d, Total Amount: %.2f\n", totalSales, totalAmount)) 15 16 return builder.String() 17} 18 19func main() { 20 aggregator := SalesAggregator{sales: make(map[string]sale)} 21 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 22 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 23 24 fmt.Println(aggregator.FormatSales(50)) 25 // Output: 26 // Sales: 27 // Sale ID: 001, Amount: 100.50, Date: 2023-01-01 28 // Sale ID: 002, Amount: 200.75, Date: 2023-01-15 29 // Summary: 30 // Total Sales: 2, Total Amount: 301.25 31}
Let's analyze this code:
- The
FormatSales
method constructs a formatted string of sales data, filtered by a specified minimum amount. - It calculates total sales and amount for sales above
minAmount
usingAggregateSales
. - A
strings.Builder
is used to build the output string efficiently. - The
WriteString
method is called on the builder to append sections of the string, including sales details and summary. - The method filters sales, writing sale ID, amount, and date formatted as "YYYY-MM-DD" to the builder for qualifying sales.
- Finally, a summary line is appended, and the constructed string is returned.
Now, let's finally implement GetSalesInDateRange
, which relies on Go's time
package to filter sales records.
Go1// ... previous code 2 3type SaleDetail struct { 4 saleID string 5 amount float64 6 date time.Time 7} 8 9 10func (s *SalesAggregator) GetSalesInDateRange(startDate, endDate time.Time) []SaleDetail { 11 var result []SaleDetail 12 13 for saleID, sale := range s.sales { 14 if !sale.date.Before(startDate) && !sale.date.After(endDate) { 15 result = append(result, SaleDetail{saleID: saleID, amount: sale.amount, date: sale.date}) 16 } 17 } 18 return result 19} 20 21func main() { 22 aggregator := SalesAggregator{sales: make(map[string]sale)} 23 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 24 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 25 26 salesInRange := aggregator.GetSalesInDateRange(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC)) 27 for _, sale := range salesInRange { 28 fmt.Printf("Sale ID: %s, Amount: %.2f, Date: %s\n", sale.saleID, sale.amount, sale.date.Format("2006-01-02")) 29 } 30 // Output: 31 // Sale ID: 001, Amount: 100.50, Date: 2023-01-01 32 // Sale ID: 002, Amount: 200.75, Date: 2023-01-15 33}
In this code:
- The
GetSalesInDateRange
method filters sales records within a specified date range using thetime
package. - It accepts
startDate
andendDate
, both of typetime.Time
, which define the inclusive date range. - An empty slice of
SaleDetail
structs is initialized to store matching sales records. - The method iterates over the
sales
map and uses thetime.Time
methodsBefore
andAfter
to check if each sale's date falls within the range. - Sale records within the range are appended to the
result
slice asSaleDetail
structs, which is ultimately returned.
Congratulations! You've extended a basic sales aggregator in Go to an advanced aggregator capable of filtering, aggregating, and formatting data using custom structs and Go's built-in features. These skills are pivotal in efficiently managing data streams, especially with large datasets in Go applications. Feel free to experiment with similar challenges to deepen your understanding. Well done!