liveBook · Manning

Habit entity

As explained in Learn Go with Pocket-Sized Projects

A habit entity is a tracked behavior represented by a name and a weekly frequency.

A habit is a behavior to track, identified by a name and a target number of times per week. For example, “practice Go” with a weekly frequency of five describes an objective, not five completed sessions. Creating, listing, ticking, and checking habits turns that objective into a weekly tracker. section 10.1.1 section 10.7

Listing 10.3 habit.proto: Defining the Habit message
// Habit represents an objective one wants to complete 
// a given number of times per week.
message Habit { #1
    // Name of the habit, cannot be empty
    string name = 1; #2
    // Frequency, expressed in times per week. 
    int32 weekly_frequency = 2;
}

The proto3 Habit message defines the shared API fields: a nonempty name and a weekly frequency measured in times per week. section 10.1.1

Keep the business entity separate from the Protobuf message. The Go domain type adds an ID for finding the habit again and a creation time, neither of which comes from the creation input. Distinct ID, Name, and WeeklyFrequency types also make arguments harder to confuse. section 10.3.1

Listing 10.13 internal/habit/create.go: Validate/complete entity
// validateAndCompleteHabit fills the habit with values that we want in 
// our database.
// Returns InvalidInputError. #1
func validateAndCompleteHabit(h Habit) (Habit, error) {
    // name cannot be empty
    h.Name = Name(strings.TrimSpace(string(h.Name))) #2
    if h.Name == "" {
        return Habit{}, InvalidInputError{ #3
            field: "name", reason: "cannot be empty"}
    }

    if h.WeeklyFrequency == 0 {
        h.WeeklyFrequency = 1 #4
    }

    if h.ID == "" { 
        h.ID = ID(uuid.NewString()) #5
    }

    if h.CreationTime.Equal(time.Time{}) {
        h.CreationTime = time.Now()
    }

    return h, nil
}

Before saving a habit, domain validation trims its name, rejects an empty result, defaults zero weekly frequency to one, and fills missing ID and creation time. Keeping this work in the domain also protects callers that bypass the gRPC API. section 10.3.1

A tick records an achievement; it is not another field on the habit. Ticks are stored separately so a list of objectives and a habit’s status for a week can be retrieved independently. TickHabit uses the habit ID, while GetHabitStatus returns the habit with its tick count. section 10.7.1 section 10.7.6

From target to progress

Create “Write some Go code” with weekly_frequency set to 3. Its response supplies an ID. Pass that ID to TickHabit, then call GetHabitStatus: the habit still has a target of three, and ticksCount is one. A timestamp can select another week for ticking and status checks. section 10.7.6

A frontend can use a different Habit shape: ID, name, weekly frequency, and this week’s tick count instead of creation time. A gRPC client translates backend responses into that shape; a Go template can then display each habit’s exposed Name and WeeklyFrequency fields. section 11.1.3 section 11.2.2

How Learn Go with Pocket-Sized Projects covers it

How deep it goes
Defines Protobuf and Go representations, then works through domain validation, defaults, and tracking; leaves tick and status implementation as a guided exercise.
Examples
Uses Go practice and reading targets, grpcurl requests, status responses, and integration-test helpers.
What it assumes
Assumes Go module setup while introducing the Protobuf basics needed to declare messages and fields.
What you can do after
Build it from scratch as a validated Go entity exposed through gRPC and used to track weekly progress.
How it connects
Connects Protobuf messages, gRPC services, domain validation, tick counts, and Go HTML templates.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Learn Go with Pocket-Sized Projects. The section links point to the parts of the book it draws on.