Go Learning Diary: Entry 1

Finally organized myself to start learning Go. As expected, I decided to start practice right away in order to get better at using the language. I came up with a "laboratory work" in which I plan to consolidate various aspects of the language, while not forgetting the existing experience of development in other languages, in particular - various architectural principles, including SOLID and others. I am writing this article in the course of the implementation of the idea itself, voicing my main thoughts and considerations on how to do this or that part of the work. So this is not a lesson-type article where I try to teach someone how and what to do, but rather just a log of my thoughts and reasoning for history, so that there was something to refer to later when doing work on mistakes.

Introductory

The essence of the laboratory is to keep a diary of cash expenditures using a console application. The functionality is preliminary as follows:

  • the user can make a new expense record both for the current day and for a day in the past, indicating the date, amount and comment

  • it can also make selections by dates, getting the total amount spent at the output

Formalization

So, according to business logic, we have two entities: a separate expense record ( Expense ) and the general entity Diary , which personifies the spending diary as a whole. Expense consists of fields such as date , sum and comment . Diary does not yet consist of anything and simply personifies the diary itself as a whole, in one way or another containing a set of Expense objects , and accordingly allows them to be obtained / modified for various purposes. Its further fields and methods will be seen below. Since we are talking about a sequential list of records, especially ordered by dates, an implementation in the form of a linked list of entities suggests itself. And in this case the objectDiary can only refer to the first item in the list. It also needs to add basic methods for manipulating elements (add / remove, etc.), but you shouldn't go overboard with filling this object so that it doesn't take on too much , that is, it doesn't contradict the principle of single responsibility (Single responsibility - the letter S in SOLID). For example, you should not add methods to save the diary to a file or read from it. As well as any other specific methods of analysis and data collection. In the case of a file, this is a separate layer of architecture (storage) that is not directly related to business logic. In the second case, the options for using the diary are unknown in advance and can vary greatly., which will inevitably lead to constant changes in Diary , which is very undesirable. Therefore, all additional logic will be outside of this class.

Closer to the body, that is, the realization

In total, we have the following structures, if we land even more and talk about a specific implementation in Go:

//     
type Expense struct {
  Date time.Date
  Sum float32
  Comment string
}

//  
type Diary struct {
  Entries *list.List
}

It is better to work with linked lists with a generic solution such as the container / list package . These structure definitions should be put into a separate package, which we will call expenses : let's create a directory inside our project with two files: Expense.go and Diary.go.

/ , / . , : ( ), - -, , , . . , , . : Save(d *Diary) Load() (*Diary). : DiarySaveLoad, expenses/io:

type DiarySaveLoad interface {
	Save(diary *expenses.Diary)
	Load() *expenses.Diary
}

, /, / (, , - - URL , ). , . , (Liskov substitution - L SOLID), . -, / , : Save Load . , , , , , , DiarySaveLoadParameters, /, . . (Interface segregation - I SOLID), , .

, : FileSystemDiarySaveLoad. , “ ”, - / :

package io

import (
	"expenses/expenses"
	"fmt"
	"os"
)

type FileSystemDiarySaveLoad struct {
	Path string
}

func (f FileSystemDiarySaveLoad) Save(d *expenses.Diary) {
	file, err := os.Create(f.Path)
	if err != nil {
		panic(err)
	}

	for e := d.Entries.Front(); e != nil; e = e.Next() {
		buf := fmt.Sprintln(e.Value.(expenses.Expense).Date.Format(time.RFC822))
		buf += fmt.Sprintln(e.Value.(expenses.Expense).Sum)
		buf += fmt.Sprintln(e.Value.(expenses.Expense).Comment)
		if e.Next() != nil {
			buf += "\n"
		}

		_, err := file.WriteString(buf)
		if err != nil {
			panic(err)
		}
	}
	err = file.Close()
}

:

func (f FileSystemDiarySaveLoad) Load() *expenses.Diary {
	file, err := os.Open(f.Path)
	if err != nil {
		panic(err)
	}

	scanner := bufio.NewScanner(file)
	entries := new(list.List)
	var entry *expenses.Expense
	for scanner.Scan() {
		entry = new(expenses.Expense)
		entry.Date, err = time.Parse(time.RFC822, scanner.Text())
		if err != nil {
			panic(err)
		}
		scanner.Scan()
		buf, err2 := strconv.ParseFloat(scanner.Text(), 32)
		if err2 != nil {
			panic(err2)
		}
		entry.Sum = float32(buf)
		scanner.Scan()
		entry.Comment = scanner.Text()
		entries.PushBack(*entry)
		entry = nil
		scanner.Scan() // empty line
	}

	d := new(expenses.Diary)
	d.Entries = entries

	return d
}

“ ”, / . , , expenses/io/FileSystemDiarySaveLoad_test.go:

package io

import (
	"container/list"
	"expenses/expenses"
	"math/rand"
	"testing"
	"time"
)

func TestConsistentSaveLoad(t *testing.T) {
  path := "./test.diary"
  d := getSampleDiary()
	saver := new(FileSystemDiarySaveLoad)
	saver.Path = path
	saver.Save(d)

	loader := new(FileSystemDiarySaveLoad)
	loader.Path = path
	d2 := loader.Load()

	var e, e2 *list.Element
	var i int

	for e, e2, i = d.Entries.Front(), d2.Entries.Front(), 0; e != nil && e2 != nil; e, e2, i = e.Next(), e2.Next(), i+1 {
		_e := e.Value.(expenses.Expense)
		_e2 := e2.Value.(expenses.Expense)

		if _e.Date != _e2.Date {
			t.Errorf("Data mismatch for entry %d for the 'Date' field: expected %s, got %s", i, _e.Date.String(), _e2.Date.String())
		}
    //      Expense ...
	}

	if e == nil && e2 != nil {
		t.Error("Loaded diary is longer than initial")
	} else if e != nil && e2 == nil {
		t.Error("Loaded diary is shorter than initial")
	}
}

func getSampleDiary() *expenses.Diary {
	testList := new(list.List)

	var expense expenses.Expense

	expense = expenses.Expense{
		Date:    time.Now(),
		Sum:     rand.Float32() * 100,
		Comment: "First expense",
	}
	testList.PushBack(expense)

  //    
  // ...

	d := new(expenses.Diary)
	d.Entries = testList

	return d
}

, , . , /: , , . go test expenses/expenses/io -v

FAIL :

Data mismatch for entry 0 for the 'Date' field: expected 2020-09-14 04:16:20.1929829 +0300 MSK m=+0.003904501, got 2020-09-14 04:16:00 +0300 MSK

: . , time.Now, . : / RFC822, , , . . , , , , ( ), . . , . SOLID, , (Open-closed principle - O SOLID). , . , -, . , , , - , , Expense. , Go , expenses:

func Create(date time.Time, sum float32, comment string) Expense {
	return Expense{Date: date.Truncate(time.Second), Sum: sum, Comment: comment}
}

, Expense ( :D), : Load FileSystemDiarySaveLoad, ( getSampleDiary). . , , , , time.RFC3339Nano . , , , .

. :) , / , , . :) , Diary, . . ( container/list) - "" Diary, - . () Diary, , , . .

, Go, , - Go. , , : , . , . , :)

PS The repository with the project is located at https://github.com/Amegatron/golab-expenses . The master branch will contain the most recent version of the work. Tags ( tags ) will mark the last commit made in accordance with each article. For example, the last commit according to this article (entry 1) would be tagged stage_01 .




All Articles