현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

Methods in Go

Go supports some object-orient programming features. Method is one of these features. This article will introduce method related concepts in Go.

Method Declarations

In Go, we can (explicitly) declare a method for type T and *T, where T must satisfy 4 conditions:
  1. T must be a defined type;
  2. T must be defined in the same package as the method declaration;
  3. T must not be a pointer type;
  4. T must not be an interface type. Interface types will be explained in the next article.

Type T and *T are called the receiver type of the respective methods declared for them. Type T is called the receiver base types of all methods declared for both type T and *T.

Note, we can also declare methods for type aliases of the T and *T types specified above. The effect is the same as declaring methods for the T and *T types themselves.

If a method is declared for a type, we can say the type has (or owns) the method.

From the above listed conditions, we will get the conclusions that we can never (explicitly) declare methods for:

A method declaration is similar to a function declaration, but it has an extra parameter declaration part. The extra parameter part can contain one and only one parameter of the receiver type of the method. The only one parameter is called a receiver parameter of the method declaration. The receiver parameter must be enclosed in a () and declared between the func keyword and the method name.

Here are some method declaration examples:
// Age and int are two distinct types. We
// can't declare methods for int and *int,
// but can for Age and *Age.
type Age int
func (age Age) LargerThan(a Age) bool {
	return age > a
}
func (age *Age) Increase() {
	*age++
}

// Receiver of custom defined function type.
type FilterFunc func(in int) bool
func (ff FilterFunc) Filte(in int) bool {
	return ff(in)
}

// Receiver of custom defined map type.
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
	_, present := ss[key]
	return present
}
func (ss StringSet) Add(key string) {
	ss[key] = struct{}{}
}
func (ss StringSet) Remove(key string) {
	delete(ss, key)
}

// Receiver of custom defined struct type.
type Book struct {
	pages int
}

func (b Book) Pages() int {
	return b.pages
}

func (b *Book) SetPages(pages int) {
	b.pages = pages
}

From the above examples, we know that the receiver base types not only can be struct types, but also can be other kinds of types, such as basic types and container types, as long as the receiver base types satisfy the 4 conditions listed above.

In some other programming languages, the receiver parameter names are always the implicit this, which is not a recommended identifier for receiver parameter names in Go.

The receiver of type *T is called pointer receiver, non-pointer receivers are called value receivers. Personally, I don't recommend to view the terminology pointer as an opposite of the terminology value, because pointer values are just special values. But, I am not against using the pointer receiver and value receiver terminologies here. The reason will be explained below.

Method names can be the blank identifier _. A type can have multiple methods with the blank identifier as name. But such methods can never be called. Only exported methods can be called from other packages. Method calls will be introduced in a later section.

Each Method Corresponds to an Implicit Function

For each method declaration, compiler will declare a corresponding implicit function for it. For the last two methods declared for type Book and type *Book in the last example in the last section, two following functions are implicitly declared by compiler:
func Book.Pages(b Book) int {
	// The body is the same as the Pages method.
	return b.pages
}

func (*Book).SetPages(b *Book, pages int) {
	// The body is the same as the SetPages method.
	b.pages = pages
}

In each of the two implicit function declarations, the receiver parameter is removed from its corresponding method declaration and inserted into the normal parameter list as the first one. The function bodies of the two implicitly declared functions is the same as their corresponding method explicit bodies.

The implicit function names, Book.Pages and (*Book).SetPages, are both of the form TypeDenotation.MethodName. As identifiers in Go can't contain the period special characters, the two implicit function names are not legal identifiers, so the two functions can't be declared explicitly. They can only be declared by compilers implicitly, but they can be called in user code:
package main

import "fmt"

type Book struct {
	pages int
}

func (b Book) Pages() int {
	return b.pages
}

func (b *Book) SetPages(pages int) {
	b.pages = pages
}

func main() {
	var book Book
	// Call the two implicit declared functions.
	(*Book).SetPages(&book, 123)
	fmt.Println(Book.Pages(book)) // 123
}

In fact, compilers not only declare the two implicit functions, they also rewrite the two corresponding explicit declared methods to let the two methods call the two implicit functions in the method bodies (at least, we can think this happens), just like the following code shows:
func (b Book) Pages() int {
	return Book.Pages(b)
}

func (b *Book) SetPages(pages int) {
	(*Book).SetPages(b, pages)
}

Implicit Methods With Pointer Receivers

For each method declared for value receiver type T, a corresponding method with the same name will be implicitly declared by compiler for type *T. By the example above, the Pages method is declared for type Book, so a method with the same name Pages is implicitly declared for type *Book:
// Note: this is not a legal Go syntax.
// It is shown here just for explanation purpose.
// It indicates that the expression (&aBook).Pages
// is evaluated as aBook.Pages (see below sections).
func (b *Book) Pages = (*b).Pages

This is why I don't reject the use the value receiver terminology (as the opposite of the pointer receiver terminology). After all, when we explicitly declare a method for a non-pointer type, in fact two methods are declared, the explicit one is for the non-pointer type and the implicit one is for the corresponding pointer type.

As mentioned at the last section, for each declared method, compilers will also declare a corresponding implicit function for it. So for the implicitly declared method, the following implicit function is declared by compiler.
func (*Book).Pages(b *Book) int {
	return Book.Pages(*b)
}

In other words, for each explicitly declared method with a value receiver, two implicit functions and one implicit method will also be declared at the same time.

Method Specifications and Method Sets

A method specification can be viewed as a function prototype without the func keyword. We can view each method declaration is composed of the func keyword, a receiver parameter declaration, a method specification and a method (function) body.

For example, the method specifications of the Pages and SetPages methods shown above are
Pages() int
SetPages(pages int)

Each type has a method set. The method set of a non-interface type is composed of all the method specifications of the methods declared, either explicitly or implicitly, for the type, except the ones whose names are the blank identifier _. Interface types will be explained in the next article.

For example, the method sets of the Book type shown in the previous sections is
Pages() int
and the method set of the *Book type is
Pages() int
SetPages(pages int)

The order of the method specifications in a method set is not important for the method set.

For a method set, if every method specification in it is also in another method set, then we say the former method set is a subset of the latter one, and the latter one is a superset of the former one. If two method sets are subsets (or supersets) of each other, then we say the two method sets are identical.

Given a type T, assume it is neither a pointer type nor an interface type, for the reason mentioned in the last section, the method set of a type T is always a subset of the method set of type *T. For example, the method set of the Book type shown above is a subset of the method set of the *Book type.

Please note, non-exported method names, which start with lower-case letters, from different packages will be always viewed as two different method names, even if the two method names are the same in literal.

Method sets play an important role in the polymorphism feature of Go. About polymorphism, please read the next article (interfaces in Go) for details.

The method sets of the following types are always blank:

Method Values and Method Calls

Methods are special functions actually. Methods are often called member functions. When a type owns a method, each value of the type will own an immutable member of a function type. The member name is the same as the method name and the type of the member is the same as the function declared with the form of the method declaration but without the receiver part.

A method call is just a call to such a member function. For a value v, its method m can be represented with the selector form v.m, which is a function value.

An example containing some method calls:
package main

import "fmt"

type Book struct {
	pages int
}

func (b Book) Pages() int {
	return b.pages
}

func (b *Book) SetPages(pages int) {
	b.pages = pages
}

func main() {
	var book Book

	fmt.Printf("%T \n", book.Pages)       // func() int
	fmt.Printf("%T \n", (&book).SetPages) // func(int)
	// &book has an implicit method.
	fmt.Printf("%T \n", (&book).Pages) // func() int

	// Call the three methods.
	(&book).SetPages(123)
	book.SetPages(123) // equivalent to the last line
	fmt.Println(book.Pages())    // 123
	fmt.Println((&book).Pages()) // 123
}

(Different from C language, there is not the -> operator in Go to call methods with pointer receivers, so (&book)->SetPages(123) is illegal in Go.)

Wait! Why does the line book.SetPages(123) in the above example compile okay? After all, the method SetPages is not declared for the Book type. One one hand, this can be viewed as a syntactic sugar to make programming convenient. This sugar only works for addressable value receivers. Compiler will implicitly take the address of the addressable value book when it is passed as the receiver argument of a SetPages method call. On the other hand, we should also think aBookExpression.SetPages is always a legal selector (from the syntax view), even if the expression aBookExpression is evaluated as an unaddressable Book value, for which case, the selector aBookExpression.SetPages is invalid (but legal).

As above just mentioned, when a method is declared for a type, each value of the type will own a member function. Zero values are not exceptions, whether or not the zero values of the types are represented by nil.

Example:
package main

type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
	// Never panic here, even if ss is nil.
	_, present := ss[key]
	return present
}

type Age int
func (age *Age) IsNil() bool {
	return age == nil
}
func (age *Age) Increase() {
	*age++ // If age is a nil pointer, then
	       // dereferencing it will panic.
}

func main() {
	_ = (StringSet(nil)).Has   // will not panic
	_ = ((*Age)(nil)).IsNil    // will not panic
	_ = ((*Age)(nil)).Increase // will not panic

	_ = (StringSet(nil)).Has("key") // will not panic
	_ = ((*Age)(nil)).IsNil()       // will not panic

	// This following line will panic. But the
	// panic is not caused by invoking the method.
	// It is caused by the nil pointer dereference
	// within the method body.
	((*Age)(nil)).Increase()
}

Receiver Arguments Are Passed by Copy

Same as general function arguments, the receiver arguments are also passed by copy. So, the modifications on the direct part of a receiver argument in a method call will not be reflected to the outside of the method.

An example:
package main

import "fmt"

type Book struct {
	pages int
}

func (b Book) SetPages(pages int) {
	b.pages = pages
}

func main() {
	var b Book
	b.SetPages(123)
	fmt.Println(b.pages) // 0
}

Another example:
package main

import "fmt"

type Book struct {
	pages int
}

type Books []Book

func (books Books) Modify() {
	// Modifications on the underlying part of
	// the receiver will be reflected to outside
	// of the method.
	books[0].pages = 500
	// Modifications on the direct part of the
	// receiver will not be reflected to outside
	// of the method.
	books = append(books, Book{789})
}

func main() {
	var books = Books{{123}, {456}}
	books.Modify()
	fmt.Println(books) // [{500} {456}]
}

Some off topic, if the two lines in the orders of the above Modify method are exchanged, then both of the modifications will not be reflected to outside of the method body.
func (books Books) Modify() {
	books = append(books, Book{789})
	books[0].pages = 500
}

func main() {
	var books = Books{{123}, {456}}
	books.Modify()
	fmt.Println(books) // [{123} {456}]
}

The reason here is that the append call will allocate a new memory block to store the elements of the copy of the passed slice receiver argument. The allocation will not reflect to the passed slice receiver argument itself.

To make both of the modifications be reflected to outside of the method body, the receiver of the method must be a pointer one.
func (books *Books) Modify() {
	*books = append(*books, Book{789})
	(*books)[0].pages = 500
}

func main() {
	var books = Books{{123}, {456}}
	books.Modify()
	fmt.Println(books) // [{500} {456} {789}]
}

Method Value Normalization

At compile time, compilers will normalize each method value expression, by changing implicit address taking and pointer dereference operations into explicit ones in that method value expression.

Assume v is a value of type T and v.m is a legal method value expression, Assume p is a value of type *T and p.m is a legal method value expression,

Promoted method value Normalization will be explained in the following type embedding article.

Method Value Evaluation

Assume v.m is a normalized method value expression, at run time, when the method value v.m is evaluated, the receiver argument v is evaluated and a copy of the evaluation result is saved and used in later calls to the method value.

For example, in the following code,
package main

import "fmt"

type Book struct {
	pages int
}

func (b Book) Pages() int {
	return b.pages
}

func (b *Book) Pages2() int {
	return (*b).Pages()
}

func main() {
	var b = Book{pages: 123}
	var p = &b
	var f1 = b.Pages
	var f2 = p.Pages
	var g1 = p.Pages2
	var g2 = b.Pages2
	b.pages = 789
	fmt.Println(f1()) // 123
	fmt.Println(f2()) // 123
	fmt.Println(g1()) // 789
	fmt.Println(g2()) // 789
}

A Defined Type Doesn't Obtain the Methods Declared Explicitly for the Source Type Used in Its Definition

For example, in the following code, unlike the defined type MyInt, the defined type Age has not an IsOdd method.
package main

type MyInt int
func (mi MyInt) IsOdd() bool {
	return mi%2 == 1
}

type Age MyInt

func main() {
	var x MyInt = 3
	_ = x.IsOdd() // okay
	
	var y Age = 36
	// _ = y.IsOdd() // error: y.IsOdd undefined
	_ = y
}

Should a Method Be Declared With Pointer Receiver or Value Receiver?

Firstly, from the last section, we know that sometimes we must declare methods with pointer receivers.

In fact, we can always declare methods with pointer receivers without any logic problems. It is just a matter of program performance that sometimes it is better to declare methods with value receivers.

For the cases value receivers and pointer receivers are both acceptable, here are some factors needed to be considered to make decisions.

If it is hard to make a decision whether a method should use a pointer receiver or a value receiver, then just choose the pointer receiver way.


Index↡

The Go 101 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: