Suggested changes.#1
Conversation
bobg
left a comment
There was a problem hiding this comment.
Overall this code looks very good - it's clear, well-laid-out, and looks correct.
Here are some suggestions about ways to make it more idiomatic, and in some places simpler and/or more efficient.
You should consider adding a bunch of unit tests.
| Installation (MacOs only currently): | ||
|
|
||
| brew install danwlker/remind/remind@0.0.2-alpha | ||
| ```sh |
There was a problem hiding this comment.
Use this kind of Markdown quoting to show example shell commands.
| Alternatively, with Go installed: | ||
|
|
||
| ```sh | ||
| go install github.com/DanWlker/remind@latest |
There was a problem hiding this comment.
This is the standard way to install Go programs from source, if you have go installed yourself.
| module github.com/DanWlker/remind | ||
|
|
||
| go 1.22.4 | ||
| go 1.21 |
There was a problem hiding this comment.
According to Mingo, this code doesn't need a newer version of Go than 1.21 in order to compile.
| errAddTodoAndAssociateTo := addTodoAndAssociateTo("", args) | ||
| if errAddTodoAndAssociateTo != nil { | ||
| return fmt.Errorf("addTodoAndAssociateTo: %w", errAddTodoAndAssociateTo) | ||
| if err := addTodoAndAssociateTo("", args); err != nil { |
There was a problem hiding this comment.
Always use the name err for error variables, except in the rare case when you need to distinguish between multiple error values at the same time.
This name is so idiomatic that anything else is unnecessarily hard to read.
There was a problem hiding this comment.
I was wondering about this, if there are multiple errors for a single function, whats the correct way of naming the different errors altogether? Is there a recommended format to stick to?
There was a problem hiding this comment.
Just keep reusing err, as I've done here. This is safe as long as you're all finished with one value of err before setting it to a new value.
Of course if you do need to refer to an old error value, you'll need a different name. For example, you might want to accumulate errors with errors.Join, in which case your code will need to look something like this:
result1, err := firstOperation()
result2, err2 := secondOperation()
err = errors.Join(err, err2) // note, this deals with nils so you don't have to
...
return overallResult, errOr if you prefer:
result1, err := firstOperation()
overallErr := err
result2, err := secondOperation()
overallErr = errors.Join(overallErr, err)
...
return overallResult, overallErrIncidentally, the same thing goes for variables of type context.Context: they should almost always be named ctx, even when you're creating a new one from an old one with something like:
ctx, cancel := context.WithCancel(ctx)Safe as long as you don't need the old value again, which in practice you seldom do.
| homeRemCurrProExDir, errHomeRemCurrProExDir := shared.GetHomeRemovedWorkingDir() | ||
| if errHomeRemCurrProExDir != nil { | ||
| return fmt.Errorf("helper.GetHomeRemovedCurrentProgramExecutionDirectory: %w", errHomeRemCurrProExDir) | ||
| dir, err := shared.GetHomeRemovedWorkingDir() |
There was a problem hiding this comment.
Prefer short variable names, which paradoxically improve readability most of the time.
As a rule of thumb, the closer together the earliest and the latest uses of a variable name are, the shorter it can and should be. (Conversely, when uses of a variable are separated by a lot of space, you need to make the name more descriptive.)
| func (e *FilePathNotStartsWithHome) Error() string { | ||
| return fmt.Sprintf("File path %v does not start with: %v", e.FileStr, e.HomeStr) | ||
| func (e NotUnderHomeError) Error() string { | ||
| return fmt.Sprintf("file path %v does not start with: %v", e.File, e.Home) |
There was a problem hiding this comment.
Don't capitalize error strings. See https://go.dev/wiki/CodeReviewComments#error-strings
| func GetFileContents() ([]RecordEntity, error) { | ||
| recordFile, err := GetFile() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("GetRecordFile: %w", err) |
There was a problem hiding this comment.
I think this was previously returning []RecordEntity{}. But nil is the better way to express "an empty slice." It works the same way in all the contexts you care about - len, append, etc. - and unlike []RecordEntity{} does not allocate an empty backing array for the slice header to point to.
| return RecordEntity{}, fmt.Errorf("GetDataFolder: %w", err) | ||
| } | ||
|
|
||
| newFile, err := os.CreateTemp(dataFolder, "*"+config.DefaultDataFileExtension) |
There was a problem hiding this comment.
This looks like it's going to leak tempfiles. Who/what is responsible for cleaning these up?
There was a problem hiding this comment.
i was using this as a way to generate non conflicting file names directly inside the data folder, so in this case it counts as persistent data. Though I think it does not prevent this api from being used incorrectly. Perhaps i should split this into two different functions? Hmmm let me think about it
There was a problem hiding this comment.
Ah! Clever. I totally overlooked that you were passing in dataFolder as the first argument. I'm so accustomed to that being "", and for this function being used exclusively for temp files (as the name implies).
In light of that misunderstanding, I suggest adding a comment here to prevent confusion in the next reader of this code - or in yourself when you return to this code years from now!
| var editor = "vim" | ||
|
|
||
| func init() { | ||
| if s := os.Getenv("VISUAL"); s != "" { |
There was a problem hiding this comment.
Use VISUAL in preference to EDITOR when available. See e.g. https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference
| } | ||
| defer os.Remove(tempFile.Name()) | ||
|
|
||
| if _, err := tempFile.Write(data); err != nil { |
There was a problem hiding this comment.
I think this was previously enclosed in an if len(data) != 0. But that test is unnecessary. If len(data) is 0, the call to tempFile.Write will just be a no-op.
|
Hi wow first let me say thanks for taking the time to review it. I will go through it in the next few days. Thanks again |
| if item != "" { | ||
| var ( | ||
| todoList []data.TodoEntity | ||
| sc = bufio.NewScanner(bytes.NewReader(result)) |
| const DEFAULT_CONFIG_FULL_FILE_NAME = string(os.PathSeparator) + DEFAULT_CONFIG_FILE_NAME + "." + DEFAULT_CONFIG_FILE_TYPE | ||
| DefaultConfigFileType = "yaml" | ||
| DefaultConfigFileName = ".remind" | ||
| DefaultConfigFullFileName = string(os.PathSeparator) + DefaultConfigFileName + "." + DefaultConfigFileType |
There was a problem hiding this comment.
Yeah I actually started this thinking of using markdown as the data file type and yaml as the config file type, I think for the config case you're probably right its redundant
| "strings" | ||
|
|
||
| "github.com/spf13/viper" | ||
| "gopkg.in/yaml.v3" |
There was a problem hiding this comment.
I have considered this library when searching for yaml library, but I saw its last commit was around 2 years ago so I'm not sure if its abandoned or the project is just complete as it is. So i went with the other one
There was a problem hiding this comment.
hmmm this was intended to be a cli app only, I just kept the cmd folder there as that's where cobra added the files when running the cobra-cli add command
| return RecordEntity{}, fmt.Errorf("GetDataFolder: %w", err) | ||
| } | ||
|
|
||
| newFile, err := os.CreateTemp(dataFolder, "*"+config.DefaultDataFileExtension) |
There was a problem hiding this comment.
i was using this as a way to generate non conflicting file names directly inside the data folder, so in this case it counts as persistent data. Though I think it does not prevent this api from being used incorrectly. Perhaps i should split this into two different functions? Hmmm let me think about it
| return fmt.Errorf("in yaml encoding: %w", err) | ||
| } | ||
|
|
||
| if err := f.Close(); err != nil { |
There was a problem hiding this comment.
I was wondering why this extra call was here although there was a defer. Then i experimented with returning errors from defer, but I only managed to get it to work if the error is declared in the function signature
This works:
func oneFunc() (err error) {
err = fmt.Errorf("error 1")
defer func() {
err = errors.Join(err, fmt.Errorf("error 2"))
}()
return err
}This does not work:
func oneFunc() error {
err := fmt.Errorf("error 1")
defer func() {
err = errors.Join(err, fmt.Errorf("error 2"))
}()
return err
}Is this a good way to go about it?
There was a problem hiding this comment.
Yes it is. If you want a deferred function to affect the outer function's return value, then as you discovered, you need to use named result parameters. (See https://go.dev/wiki/CodeReviewComments#named-result-parameters.) And errors.Join is a good way to preserve both the error that made your function return (if any), and the error encountered while cleaning up.
Most of the time I prefer how simple defer f.Close() looks, and when the surrounding function returns due to an error, I'm OK losing any error from the Close call. But when the function is about to return normally, I prefer to capture any error from Close, which is why there's also an explicit (non-deferred) call near the end.
(In that case, the redundant deferred Close still runs, but it's a no-op since the file's already closed, and conveniently the "already-closed" error is disregarded.)
| return "", fmt.Errorf("os.UserHomeDir: %w", err) | ||
| } | ||
|
|
||
| rel, err := filepath.Rel(home, filePathWithHome) |
Suggested edits, as requested at https://www.reddit.com/r/golang/comments/1dx7hro/a_code_review_request_for_beginner_go_cli_app/