JavaRush /Courses /Go SELF /Methods in Go — receiver and naming conventions

Methods in Go — receiver and naming conventions

Go SELF
Level 22, Lesson 0
Available

1. Why do we need methods at all if we already have functions?

If you’ve only just gotten used to functions, the appearance of methods can look like “oh no, OOP again.” But in Go, methods are more about packaging logic next to data than about inheritance, virtual table magic, and other drama.

We want actions on an entity to look like actions on an entity: task.MarkDone() reads more clearly than markDone(task) — especially when there are lots of such actions.

Imagine we have a small learning app: a “task list” (task list). For now it lives in main, and that’s fine: we’re not building an architecture yet, we’re learning to express ideas in code.

Minimal task model:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func main() {
	t := Task{ID: 1, Title: "Get the hang of methods", Done: false}
	fmt.Println(t.Title) // Get the hang of methods
}

Right now we have data (Task), but no convenient “dictionary of actions.” Methods provide exactly that dictionary.

2. Method and receiver: syntax, naming, and API readability

A method is a function that has a receiver

From a technical point of view, a method is a function with a special parameter in parentheses before the name: the receiver. The receiver is often called the “receiver” because it “receives the call.”

Psychologically, it’s convenient to think of it like this: you “call the object,” not “call a utility.” But don’t forget: in Go it’s all pretty down-to-earth — it’s a function with special syntax.

Here’s what a method that returns a “pretty name” for a task looks like:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func (t Task) Label() string {
	return fmt.Sprintf("#%d %s", t.ID, t.Title)
}

func main() {
	t := Task{ID: 1, Title: "Get the hang of methods"}
	fmt.Println(t.Label()) // #1 Get the hang of methods
}

The call happens via a dot: t.Label(). Visually, it looks like field access t.Title, just with parentheses.

A useful “demystifying” thought: a method is a function that you call in the form value.Method(). If Go had an “annoyingly pedantic compiler mode for beginners,” it could mentally rewrite this as a function call where t is passed as the first argument.

We’re not abusing this idea right now; we just keep it in mind.

How to choose a receiver name: short, stable, no creativity

A receiver is a variable, so the usual readability rules apply. There’s just one important detail here: the receiver appears in every method of the type, so even a small “stylistic mistake” gets replicated hundreds of times.

That’s exactly why the Go community loves short receiver names: it’s not laziness, it’s care for the reader’s eyes (including your future self).

Let’s agree on a practical style:

  • The receiver is almost always named with one or two letters, consistently across all methods of the type.
  • For Task, that’s naturally t. For User — u. For Order — o.
  • For something composite like TaskStore — s or st.

If you name the receiver taskEntityAggregateRoot, you’re certainly being imaginative… but your code will start to look like a legal document.

Compare:

package main

import "strings"

type Task struct {
	Title string
}

func (t Task) TitleUpper() string {
	// short and familiar
	return strings.ToUpper(t.Title)
}

and this (don’t do this):

package main

import "strings"

type Task struct {
	Title string
}

func (task Task) TitleUpper() string {
	// it technically works, but it’s harder to read
	return strings.ToUpper(task.Title)
}

The point isn’t that task is a “bad word.” The point is that the receiver is a utility variable, not the main idea of the method. The main idea is the method name and what it does.

One more important nuance: the receiver must not clash with other variables. If you have a method func (t Task) Rename(title string), then the parameter title is fine.

But if you write func (t Task) Rename(t string), you’re setting yourself up for a mini-quest: “guess which is which.”

To reinforce the rule, here’s a small table (not as law, but as good habits):

Type Good receiver Bad receiver Why
Task
t
task, theTask
wordy, adds visual noise
User
u
userObject
extra words don’t add meaning
TaskStore
s or st
storeForTasksInMemory
the receiver is not a place for documentation
http.HandlerFunc-like
h or fn
handlerFunctionThat...
the name should help you read code, not weigh it down

Interesting fact: in Go, a receiver can be not only a struct. For example, you can declare a method on a function type, and then it makes sense to name the receiver fn (function). This appears in real Go code and looks quite natural.

Method naming: how to make an API readable “like a sentence”

A method name is your mini-API. The way you name it determines whether the code reads like a coherent story or like a set of random spells.

In Go, it’s common for method calls to look like natural phrases: task.Done is a field, while task.MarkDone() is an action.

If an action “returns a value,” the name often looks like a noun: ID(), Title(), Label().

In our task app, we can start with a couple of “reading” methods:

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

func (t Task) IsDone() bool { // question => boolean answer
	return t.Done
}

And it’s immediately clear that IsDone() is a “check,” not a “change.”

For “actions,” verbs are typically used:

package main

type Task struct {
	Title string
	Done  bool
}

func (t *Task) MarkDone() {
	t.Done = true
}

We’re not diving into why *Task is used here — that will be the next lecture. The important idea here is naming: MarkDone sounds like an action and changes state.

Why Go doesn’t like GetX() (and when it’s still used)

If you came from Java/C#/TypeScript, your hand may automatically type GetID() and SetTitle(). In Go, it’s usually done differently: if a method simply returns a value, it’s named like a “field”:

package main

type Task struct {
	id int
}

func (t Task) ID() int {
	return t.id
}

This is more readable in real code: t.ID() looks almost like t.ID, except it signals computation/logic rather than direct field access.

SetX() exists in Go too, but it’s used carefully: when there really is “setting state” and it’s part of a clear API. If a method does something more complex (for example, validates, normalizes, changes multiple fields), it’s better to name it by meaning rather than “setting something.”

3. Receivers at the type level: consistency matters more than perfection

At this stage, it’s easy to fall into a trap: start arguing with yourself every minute — should the receiver be a value or a pointer, “what’s faster,” “what’s more correct.” We’ll definitely cover this in detail later, but even now it’s important to lock in one convention that saves projects from chaos: a type should have a predictable method style.

If some methods on Task take Task, some take *Task, some return a new Task, and some silently mutate — it becomes harder for the reader to use your type. They stop trusting the API and constantly double-check: “Will this method change the task or return a copy?”

So the practical rule for today’s lecture is: pick a style and stick to it within a type. Even if you later decide the style should change, changing it consistently is easier than living in a zoo of approaches.

For our Task, for now we’ll do this: “read” methods will be on Task, and “mutating” methods will be on *Task. This already reads well and doesn’t contradict common sense.

Receiver as a variable: don’t be afraid, but respect it

The receiver is a regular variable inside the method, and sometimes that helps make code very expressive. For example, inside a method you can work with fields, call other methods, and also use the receiver as the basis for logic.

The main thing is not to turn a method into a 40-page novel: several small methods are better than one “universal” one.

By the way, a common real-world practice: some methods make the receiver a pointer simply because it’s more convenient for consistency, or because the type is intended to represent “state.” But a detailed breakdown of “why pointer/why value” is the next topic; today we keep the focus on conventions and names.

4. Constructors: why NewTask is a function, not a method

A very common question: “Where’s the constructor?” Go doesn’t have constructors like classic OOP languages, but it has a simple, working pattern: a NewX(...) function.

Usually it is exactly a function, not a method, because a method requires a receiver, and a receiver requires an already-existing value. That becomes circular, so we use a function.

Here’s a minimal NewTask:

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

func NewTask(id int, title string) Task {
	return Task{ID: id, Title: title, Done: false}
}

And we use it:

package main

import "fmt"

func main() {
	t := NewTask(1, "Write NewTask")
	fmt.Println(t.Done) // false
}

From a naming standpoint, this reads отлично: “create a new task.”

5. Example: a small “dictionary of actions” for Task

Now we’ll carefully expand our Task by adding a few methods with good names and “normal” receivers. Try to read calls like sentences: t.Rename(...), t.MarkDone(), t.Label() — that’s the goal of the style.

Let’s start by renaming a task. The name Rename honestly says what we do:

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

func (t *Task) Rename(title string) {
	t.Title = title
}

Now let’s add a “reading” method that builds a display string. Here the name Label is fairly neutral: not too “UI,” but understandable.

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func (t Task) Label() string {
	status := "TODO"
	if t.Done {
		status = "DONE"
	}
	return fmt.Sprintf("[%s] #%d %s", status, t.ID, t.Title)
}

All that’s left is to define NewTask and MarkDone in our file (or nearby), and we already get a mini-API that reads almost like text.

MarkDone:

package main

type Task struct {
	Done bool
}

func (t *Task) MarkDone() {
	t.Done = true
}

And we use everything together:

package main

import "fmt"

func main() {
	t := NewTask(1, "Understand method naming")
	fmt.Println(t.Label()) // [TODO] #1 Understand method naming

	t.MarkDone()
	fmt.Println(t.Label()) // [DONE] #1 Understand method naming
}

Important: we’re not discussing the subtleties of pointers in depth yet, but naming alone already sets the right expectations. MarkDone() sounds like an action that changes state.

6. Typical mistakes with naming and receivers

Mistake #1: the receiver is named differently in every method.
Today func (t Task) …, tomorrow func (task Task) …, the day after tomorrow func (x Task) …. The code compiles, but the reader has to “recalibrate” their eyes every time. It’s better to pick one option and use it in all methods of the type.

Mistake #2: the receiver is too long and “smart.”
When the receiver turns into a mini-comment (taskEntity, currentTaskInList), it starts distracting from the essence of the method. In Go, the receiver is an implementation detail, so a short name almost always wins on readability.

Mistake #3: methods are named like in another language, and calls look heavy.
GetTitle(), SetDone(true), GetId() — that’s familiar after Java/C#, but in Go people more often expect Title() and MarkDone(). The call task.ID() reads more easily than task.GetID(), and this is felt especially strongly when there are dozens of such calls.

Mistake #4: the method name doesn’t reflect the action.
If a method changes state but is named like a “read” (Done()), or vice versa — it only checks but is named like a “command” (DoDone()) — the API becomes unpredictable. Good naming is when, from the method name alone, you can almost always guess what it does.

Mistake #5: trying to make the constructor a method.
Attempts to write something like func (t Task) New(...) Task usually lead to weird code and make “creation” look like an action on an already existing object. In Go, it’s idiomatic to write NewTask(...) as a function — it’s simple, readable, and widely used.

1
Task
Go SELF, level 22, lesson 0
Locked
Client profile
Client profile
1
Task
Go SELF, level 22, lesson 0
Locked
Click counter
Click counter
1
Task
Go SELF, level 22, lesson 0
Locked
Task card
Task card
1
Task
Go SELF, level 22, lesson 0
Locked
Order promo code
Order promo code
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION