AlgoMaster Logo

Methods Basics

Last Updated: May 17, 2026

12 min read

A method is a function that's attached to a specific type. Once you attach a function to a Product or a Cart, callers write cart.Total() instead of Total(cart), and the behavior travels with the type wherever it goes. This lesson covers what a method is, how it differs from a plain function, the declaration syntax, how method calls work on values, and how to attach methods to types other than structs.

From Functions to Methods

You already know how to write a function that takes a struct and does something with it. Here's a Cart type with a free function that computes its total:

That works. The function Total reads the cart, sums the prices, and returns the result. The caller writes Total(c) and gets back a number.

Now look at the call site again: Total(c). The function is named Total, but it only makes sense for a Cart. There's nothing in the name that says "this belongs to Cart". If you have a Product type that also has a Total (price times quantity, say), you'd need a different name, maybe ProductTotal, because Go doesn't allow two functions to share a name in the same package.

A method fixes both problems. You declare the function with a small extra piece of syntax that ties it to a specific type, and the function name only has to be unique within that type. The call site becomes c.Total() instead of Total(c). The same function name can exist on as many types as you like.

Two things changed. The function signature gained a chunk between func and the name: (c Cart). That's the receiver, and it says this function is attached to the Cart type. The call site lost the argument: instead of passing c as a parameter, it now sits on the left of the dot, just like a field access.

The body of the function didn't change. Inside the method, c is still the cart, and you still read its fields with c.Items. The receiver is, in effect, a parameter with a special place in the syntax.

Method Declaration Syntax

The full shape of a method declaration is:

Each piece does one thing:

  • func is the same keyword as for a regular function.
  • (receiver Type) is the receiver. It names a variable and gives its type. The receiver is what comes before the dot at the call site.
  • Name is the method name. It has to be unique among the methods on this type, but it doesn't have to be unique across the package.
  • (params) is the regular parameter list. It works exactly like a function's parameter list.
  • returns is the return type or types. Same rules as functions.
  • { body } is the function body. Inside, the receiver name acts like an ordinary parameter.

Here's a Product with a method that computes the total value of stock on hand:

Inside StockValue, the receiver is named p, and it behaves like a parameter of type Product. You read its fields with p.Price and p.Stock, you can call other methods on it, you can pass it to functions. The only thing different about the receiver is where it sits in the declaration and how callers reach the method.

The receiver name is just a name. By convention, Go developers use a short, one-or-two-letter name that matches the type: c for Cart, p for Product, o for Order. Long names like cart or theCart aren't wrong, but they're not idiomatic. Stick with the short form.

The receiver block lives between func and the method name. Everything else looks like a regular function signature. The compiler reads the receiver type and registers the method on that type's method set.

Calling a Method on a Value

You call a method using dot syntax: the value goes on the left, the method name and arguments on the right.

c.ItemCount() and c.Total() both call methods on the same cart. The method runs with c as the receiver, reads the cart's fields, and returns its result. There's no special "invocation" step; the call looks just like a field access, except with parentheses (and any arguments) after the method name.

You can pass arguments to methods just like with functions. The receiver is separate from the parameter list:

TotalWithTax takes one parameter, taxRate. The call site is c.TotalWithTax(0.08). The receiver c is on the left of the dot, the argument 0.08 is in the parameter list. Inside the method, c is the cart and taxRate is the rate.

A method can also call other methods on the same receiver. Inside TotalWithTax, you could call c.Total() and multiply by the tax rate:

TotalWithTax calls c.Total() to get the subtotal, then applies the tax. Methods composing other methods is how you build up behavior in small pieces.

In this lesson, every example uses a value receiver, which means the method works on a copy of the value. That has consequences for whether the method can modify the original. For now, treat methods as read-only views over the receiver's data.

At runtime, c.Total() binds the value on the left of the dot to the receiver name inside the method. The body executes and returns its result. From the outside, a method call looks like a single step. Under the hood, it's a function call with one extra implicit argument.

Methods and Functions Are Almost the Same Thing

Under the hood, a method is essentially a function whose first parameter is the receiver. The compiler treats c.Total() as roughly equivalent to calling a function with c as its first argument. Here are the two versions side by side:

The body is identical. The difference is the receiver syntax and what it lets you do at the call site. With the function, callers write Total(c). With the method, callers write c.Total(). That's a small change, but it adds up across a real codebase.

Here's a table of what differs and what doesn't:

PropertyFunctionMethod
Declared with funcYesYes
Has a body, params, returnsYesYes
Has a receiverNoYes
Name unique in packageYesOnly within the type
Called as Name(args)YesNo
Called as value.Name(args)NoYes
Can access value's fields directlyNo (must use param)Yes (via receiver)

The "name unique within the type" row is what unlocks the most useful pattern. You can have a Total method on Cart, another Total method on Order, another on Invoice, and none of them collide. Each lives on its own type's method set. The compiler picks the right one based on the type of the value on the left of the dot.

Two types, two Total methods, no conflict. If you wrote these as free functions, you'd have to name them CartTotal and OrderTotal to avoid a collision, and every caller would have to remember which version applies to which type. With methods, the type carries that information.

There's a second reason methods matter, and it shows up later: methods are how Go decides whether a value satisfies an interface. An interface is a set of method names and signatures, and a type satisfies the interface if it has methods that match. Functions on the same type can't fill that role; only methods can. It's worth knowing that this is the long-term payoff for the receiver syntax.

Why Methods Exist

You can see what methods do from the syntax. The harder question is why the language has them at all, since the same logic could be a free function. Three reasons come up repeatedly.

Behavior attached to data. When you write cart.Total(), the function name lives on the cart. Anyone reading the code can find every operation on a Cart by searching for func (c Cart) or by looking at the type's documentation. With free functions, those operations are scattered across the package, related to the type only by what they happen to take as a parameter. Putting the methods on the type keeps the type's API in one place.

Cleaner call sites. cart.Total().Round() reads left to right and chains naturally. The equivalent with free functions is Round(Total(cart)), which reads inside-out and stops chaining when you need to mix a method-style call with a non-method one. Most programmers find the dot-chained form easier to scan, especially in long expressions.

Interface satisfaction (preview). Methods are how Go decides whether a type can stand in for an interface. If an interface declares a method Total() float64, any type with a Total() float64 method automatically satisfies it. No implements keyword, no registration step. The receiver syntax is what enables this whole mechanism, which becomes the backbone of polymorphism in Go. For now, just know that interfaces are the reason this design exists, even if you haven't met them yet.

The cost is a small bit of new syntax, and the rule that the receiver type has to live in the same package as the method. You can't declare a method on int or on time.Time directly, because those types aren't yours. That restriction has its own workaround, which the next section covers.

Methods on Named (Defined) Types

The receiver type doesn't have to be a struct. Any type you define in the current package can have methods. Strings, ints, slices, maps, function types: any of them can carry methods as long as you give them a new named type first.

OrderStatus is a defined type whose underlying type is string. It behaves like a string at the value level, but the type system treats it as a separate type, distinct from string. That distinction is what lets you attach methods. The receiver s is an OrderStatus, and inside the method you use it like a string in the comparison.

You can do the same with a numeric type, for example tracking a discount as a percentage:

DiscountPercent is an int under the hood. The method Apply reads the receiver d, converts it to float64 for arithmetic, and computes the discounted amount. The conversion is explicit because Go doesn't mix numeric types implicitly.

Even slices and maps can carry methods, as long as you define a named type for them first:

PriceList is a defined type built on []float64. Methods like Sum and Max give it a small, focused API. The value behaves as a slice inside the method body (you can range over it, index into it, take its length), but from the outside it carries its own behavior.

There's one rule that catches beginners: you can only define methods on a type declared in the same package. The reason is technical (the compiler needs to be able to find all methods on a type by looking at one package), and the consequence is that you can't write func (s string) IsEmpty() bool { ... } directly. The standard library's string lives in the builtin package, not yours. The workaround is the pattern above: declare your own named type, and put methods on that.

The compiler error is something like cannot define new methods on non-local type string. The fix is to wrap string in a new named type and put the method on the new type.

Methods Live in the Package's Namespace

A method name doesn't collide with a function name in the same package, because the method lives on its type, not in the package's flat namespace. You can have a free function called Total and a Total method on Cart coexisting in the same file:

The compiler reads c.Total() and looks for a method named Total on the type of c, which is Cart. It finds the method, binds c to the receiver, and calls it. The compiler reads Total(c.Items) and looks in the package namespace for a function named Total. It finds the free function and calls that. The two names live in completely separate namespaces.

Two Total methods on two different types is also fine, since each method lives on its own type:

What you can't do is define two methods with the same name on the same type, even if the parameter lists differ. Go doesn't have method overloading. Each type has at most one method of a given name. If you want variants, pick distinct names: Total, TotalWithTax, TotalAfterDiscount.

The compiler reports method Total already declared. The fix is to rename one of them.

Several Methods on One Type

Most real types have more than one method. Each one focuses on a single piece of behavior, and they often build on each other. Here's a Cart with a small but useful set:

Each method is a small piece of behavior. ItemCount counts items. Subtotal sums prices. IsEmpty is a convenience that wraps a length check. TotalWithTax builds on Subtotal by multiplying. None of them is more than a few lines, and together they give a Cart a clean API that callers can use without ever reading the struct's internals.

This is the shape that real Go types take. A bytes.Buffer has methods like Write, Read, Bytes, String, Reset, Len. A time.Time has Year, Month, Day, Hour, Add, Sub, Format. Each method is small and focused. The type as a whole is defined by the set of methods it offers, not by its internal fields.

Methods Don't Need a Struct at All

The examples so far have used structs, because that's the most common case. To drive the point home that methods work on any defined type, here's a tiny program that puts methods on a named integer:

Quantity is just an int with a name and two methods. There's no struct anywhere. The receiver q is a Quantity, and inside the methods you treat it like an int for comparisons and formatting. Callers benefit from the methods because they read q.IsValid() and q.Plural() at the call site, both of which are clearer than passing a raw int around with the validation rules buried in some helper function elsewhere.

This pattern, named primitive plus methods, shows up often in real Go code for things like IDs, codes, and small enums. Often the named type carries one or two validation methods and a String method, and that's it. Small, focused, and the call sites read like English.

Summary

  • A method is a function attached to a specific type. The syntax is func (receiver Type) Name(params) returns { body }, where the receiver block sits between func and the method name.
  • Methods are called with dot syntax: value.Method(args). The value on the left of the dot becomes the receiver inside the method body.
  • Inside a method, the receiver behaves like an ordinary parameter. You read its fields, do arithmetic with it, and pass it to other functions.
  • Method names live on their receiver type, not in the package's flat namespace. Two types can both have a Total method without colliding, and a method can share a name with a free function in the same package.
  • Methods aren't limited to structs. Any type defined in the current package can carry methods, including named primitives, slices, and maps. You can't add methods to types from other packages directly; declare a local named type as a wrapper instead.
  • Go doesn't have method overloading. A given type has at most one method per name, even if parameter lists differ. Use distinct names for variants.

This chapter used value receivers throughout, which means every method ran on a copy of its receiver. The next chapter, Value vs Pointer Receivers, covers when to use each form, what each one means for mutation and copy cost, and the conventions Go developers follow for picking between them.