Go in a nutshell
  • Go in a nutshell
  • Введение
  • Родословная или Should I Go?
  • Hello, world!
  • Базовые типы данных
  • Go - установка, сборка
  • Алфавит и пакеты
  • Синтаксис
  • Приведение типов
  • Операторы
  • Константы
  • Управление потоком
  • Массивы, срезы, отображения
  • Именованные типы и структуры
  • Указатели и оператор new
  • Функции
  • Работа с параметрами командной строки
  • Завершение работы программы
  • Методы
Powered by GitBook
On this page
  • Методы в языке Go
  • Синтаксис
  • Импорт пакетов и методы
  • Методы и указатели

Was this helpful?

Методы

Снова принимаем процедуры

Начиная беседу о методах, давайте сперва - извините за "масло масляное" - определимся в определениях. Неспециализированный словарь общего назначения даст нам такую дефиницию: "метод есть способ теоретического исследования или практического осуществления чего-либо". Что ж, посмотрим, как обстоит дело в компьютерных наших науках. А дело обстоит достаточно зыбко, поскольку, от языка к языку, терминология разнится. Конкретнее, происходит некоторое взаимопереплетение понятий "метод" и "функция". Во многих (как правило, процедурного класса) языках программирования вовсе отсутствует понятие методов: это - царство функций. В таком языке, как JavaScript, методом могут назвать функцию, являющуюся свойством (иначе - полем) переменной типа object (внимание, первые звоночки ООП!). В собственно "чистокровных" (thoroughbred) объектно-ориентированных языках, каковыми являются, к примеру, C# и Java, есть методы как члены класса, глубоко связанные с самим понятием класса сущностно, но отдельно стоящие функции отсутствуют вовсе. Впрочем, в разговорной практике слова "метод" и "функция" часто не дифференцируются, и любое образование, имеющее "функциональную" сигнатуру, и, в особенности, имеющее отношение к некоему API, с равным успехом именуется методом или функцией на усмотрение говорящего. Но что ожидает нас в языке Go?

Методы в языке Go

Методы в языке Go представляют собою функции особого типа. Их использование ассоциировано сугубо со структурами и именованными типами и, наряду с так называемыми интерфейсами, представляет собой основу ООП в этом языке. Итак, метод - это функция, связанная с определённым типом (исключая базовые). Внешне, методы в Go напоминают мне чем-то методы расширения в C#. В действительности, мы с вами с момента изначального знакомства с языком Go уже полноценно пользуемся методами. Например, вездесущий fmt.Println() - есть ни что иное, как экспортируемый метод, определённый в пакете fmt.

Синтаксис

Что нам понадобится для создания метода? Во-первых, нужно продекларировать именованный тип или структуру, с которыми наш метод будет связан. Здесь я хочу подчеркнуть, что, не смотря на то, что мы уже уверенно ступили на дорогу ООП, методы в действительности могут быть довольно разнообразными по своей сути; воплощение какой концепции вы получите на выходе - вам решать. Как говаривал дедушка Фрейд, иногда сигара - это просто сигара. Поэтому начнём с простого, как бы издалека.

Но вернёмся на землю, то есть к, собственно, синтаксису. Итак, допустим, некий "подопытный" тип создан. Что дальше? Всё просто: синтаксически метод - та же функция, при декларации которой между ключевым словом func и именем функции мы добавляем «получателя» - наш ассоциированный тип. Вызов метода производится при помощи оператора "точка". Знакомая конструкция, не правда ли? Проиллюстрируем сказанное простыми примерами, не претендующими пока на явную причастность к ООП.

package main

import "fmt"

type T uint // простой именованный тип

//метод
func (t T) factorial() {
	result := 1
	for i := 2; i <= int(t); i++ {//обратите внимание на приведение типа
		result *= i
	}
	fmt.Println(result)
}

func main() {

	var t T = 5

	t.factorial()// вызов метода через оператор "."
}
//Результат:
//120

Как видите, здесь мы просто реализовали удобную математическую функцию, не претендуя в данном случае на глубокое воплощение принципов ООП. Обратите внимание, нам пришлось в данном случае отказаться от использования рекурсивного алгоритма. Отметьте для себя этот важный момент: рекурсия - неподходящий способ для методов, поскольку метод не может вызвать сам себя, но привязан лишь к ассоциированному с ним типу (структуре).

Интереса ради, расширим наш математический пример. Заставим нашу дрессированную программу разучить ещё один фокус: пускай это будет n-ное число ряда Фибоначчи:

package main

import "fmt"

type T uint // простой именованный тип

//метод factorial
func (t T) factorial() {
	result := 1
	for i := 2; i <= int(t); i++ { //обратите внимание на приведение типа
		result *= i
	}
	fmt.Println(result)
}

//метод fibonacci
func (t T) fibonacci() {
	n := int(t)

	var (
		a  int = 1
		ta int
		b  int = 1
		tb int
		c  int = 1
		rc int = 0
		tc int
		d  int = 0
		rd int = 1
	)
	
	for n != 0 {

		if n%2 != 0 { // Если степень нечетная

			// Умножаем вектор R на матрицу A
			tc = rc
			rc = rc*a + rd*c
			rd = tc*b + rd*d
		}

		// Умножаем матрицу A на саму себя
		ta = a
		tb = b
		tc = c
		a = a*a + b*c
		b = ta*b + b*d
		c = c*ta + d*c
		d = tc*tb + d*d

		n >>= 1 // Уменьшаем степень вдвое

	}	
	fmt.Println(rc)
}

func main() {

	var t T = 7

	t.factorial()
	t.fibonacci()
}
//Результат:
//5040
//13	

А что, мне уже нравится! Почему бы нет: вырисовывается некоторое подобие калькулятора... К слову, обратите внимание на избранную нами реализацию алгоритма вычисления чисел ряда Фибоначчи: это - самое эффективное решение данной задачи с помощью быстрого возведения матрицы в степень. Nota bene, sapienti sat...

Дополню изложение ещё одним примечательным примером. Рискнём, к тому же, добавить сюда гомеопатическую дозу ООП. Отчего бы нам не поработать со срезом? Как вы, вероятно, помните, благодаря возможности присвоения кортежем, существует хорошая практика работы со срезом с экономией памяти; как это называют, "на месте". Особенно полезной часто бывает реализация реверса. Сделаем же это, обличив в форму метода! Некто любознательный может поинтересоваться: в чём смысл? Поясню. Временами приходится работать с последовательностями байтов, имеющую различный так называемый endianness (порядок следования), коих, как известно, существует два: big endian и little endian. И, что самое весёлое, в иных протоколах обмена данными, может встречаться прелюбопытный микс из обоих "ордеров"! Конвертация неизбежна. Приступим.

package main

import "fmt"

type T []byte

func (t T) reverse() {
	for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {
		t[i], t[j] = t[j], t[i]
	}
}

func (t T) String() string {
	s := ""
	for _, val := range t {
		s += fmt.Sprintf("%#02x ", val)
	}
	return s
}

func main() {
	var t T = T{0x01, 0x02, 0x03}
	fmt.Println(t)
	t.reverse()
	fmt.Println(t)
}
//Результат:
//0x01 0x02 0x03 
//0x03 0x02 0x01

Вот так, вкусно и полезно. Однако, как было обещано, в данном примере для вас заготовлен небольшой ООП-сюрприз. Почему мы продекларировали метод String(), но его явного использования не заметно вовсе? Ошибки не было. Мы смогли использовать напрямую вызов fmt.Println(t) вместо ожидаемой конструкции fmt.Println(t.String()) благодаря тому, что метод String() реализует интерфейс Stringer, определённый в пакете fmt.

type Stringer interface {
    String() string
}

Как следствие, наш тип Т, реализуя интерфейс Stringer пакета fmt, позволяет нам использовать конструкцию fmt.Println(t) напрямую, получая на выходе текст, отформатированный в соответствии с правилами, определёнными в методе String. Разумеется, мы несколько забежали вперёд и более детальное обсуждение этой темы у нас ещё впереди. Однако, для оживления изложения, мы немного прикоснулись к ООП в языке Go в разрезе реализации методов стандартных интерфейсов. Запомните: для языка Go это - повсеместная практика. Впрочем, если вы по каким-то причинам не желаете пользоваться подобным функционалом, вы вполне можете оставаться в рамках парадигмы процедурного программирования. Функция printBuf() вполне заменит нам ООП - конструкцию:

package main

import (
	"fmt"
)

type T []byte

func (t T) reverse() {
	for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {
		t[i], t[j] = t[j], t[i]
	}
}

func printBuf(buffer []byte) {
	for _, val := range buffer {
		fmt.Printf("%#02x ", val)
	}
	fmt.Println()
}

func main() {
	var t T = T{0x01, 0x02, 0x03}
	printBuf([]byte(t))
	t.reverse()
	printBuf([]byte(t))
}
//Результат (он аналогичен):
//0x01 0x02 0x03 
//0x03 0x02 0x01

Выбор расстановки акцента на ООП или процедурном стиле - зачастую дело вашего личного вкуса; это также пребывает в зависимости от неких соглашений, принятых в той команде, где вы работаете. В отличие от "чистых" ООП языков, Go не принуждает вас явно к тому или иному выбору.

Далее, небесполезно будет знать, что Go допускает бесконфликтное сосуществование одноимённых функции и метода в одной области видимости. И ещё, поскольку метод привязан к своему типу и как бы входит в его пространство имён, вполне корректным приёмом является объявление одноимённых методов для различных типов, что, имея в виду понятие реализации интерфейсов, является одной из форм претворения принципов ООП. Согласитесь, выделение, к примеру, метода area() выглядит логично для любой структуры, описывающей некую геометрическую фигуру на плоскости:

package main

import (
	"fmt"
	"math"
)

//структуры - фигуры
type circle struct{ radius float64 }
type rectangle struct{ a, b float64 }
type triangle struct{ a, b, c float64 }

//одноименные методы, разные типы-получатели
func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (r rectangle) area() float64 {
	return r.a * r.b
}

//площадь треугольника по формуле Герона
func (t triangle) area() float64 {
	var p float64 = (t.a + t.b + t.c) / 2
	return math.Sqrt(p * (p - t.a) * (p - t.b) * (p - t.c))
}

func main() {
	c := circle{3}
	r := rectangle{2, 5}
	t := triangle{4, 5, 6}

	fmt.Println(c.area())
	fmt.Println(r.area())
	fmt.Println(t.area())
}
//Результат:
//28.274333882308138
//10
//9.921567416492215

Импорт пакетов и методы

package utils

//AwesomeType is just a simple named type
//(не забываем корректно комментировать экспортируемый тип!)
type AwesomeType int

Далее, создадим собственно файл программы main.go, импортируем в код вышеописанный пакет fqdn.org/common-utils и попытаемся создать метод для типа utils.AwesomeType. поскольку базовый тип - int, пускай это будет, допустим, проверка значения на чётность:

package main

import (
	"fmt"
	"fqdn.org/common-utils"
)

//проверим на чётность
func (a utils.AwesomeType) isEven() bool {
	if a%2 == 0 {
		return true
	}
	return false
}

func main() {
	var a utils.AwesomeType = 4
	fmt.Println(a.isEven())
}
//Результат:
//.\main.go:9:6: cannot define new methods on non-local type utils.AwesomeType
//.\main.go:18:15: a.isEven undefined (type utils.AwesomeType has no field or method isEven)

Результатом будет ошибка компиляции: мы не можем определять новые методы для нелокального типа, компилятор не велит! На самом деле, это очень распространённая ситуация, и раз так, необходимо решение! Решением является использование обёрток-wrapper`ов. Нужно тем или иным образом привязать требуемый внешний тип к локальному - либо методом прямого выведения, либо путём инкапсуляции в качестве поля в локальную структуру:

//Делаем так:
type T utils.AwesomeType

//Или вот так:
type S struct {
    a utils.AwesomeType
    // possible other fields
    //...
    //
}

Как результат, мы можем определить нужный метод, который, пусть и косвенно, будет ассоциироваться с нелокальным типом. Это работает:

package main

import (
	"fmt"
	"fqdn.org/common-utils"
)

type T utils.AwesomeType

//проверим на чётность
func (t T) isEven() bool {
	if t%2 == 0 {
		return true
	}
	return false
}

func main() {
	var t T = 4
	fmt.Println(t.isEven())
}
//Результат:
//true

Главное в данном случае - это понять принцип. На практике это позволит впоследствии строить действительно сложные конструкции, при необходимости.

Методы и указатели

Далее, полагаю, нам необходимо сделать акцент на одном сущностном моменте. Дело в том, что все методы можно очевидно разделить на две категории, а именно - по признаку, является ли получатель метода типом, или же указателем. Да, указатели на именованный тип или структуру могут быть использованы при объявлении метода; собственно, в этом плане метод в определённом смысле повторяет поведение обычной функции.

PreviousЗавершение работы программы

Last updated 4 years ago

Was this helpful?

Как известно, одна из основных практик языка Go - импортирование пакетов. То есть использование кода, написанного другими программистами, либо же повторное использование собственного кода, во исполнение принципа DRY - don't repeat yourself. Результатом этого является ситуация, когда в ваш код регулярно проникают "посторонние" типы. Плохой новостью является тот факт, что для типа, объявленного в другом пакете (и авторство этого пакета совершенно не важно), вы не можете создавать методы. Приведём простой пример. Некогда, в главке мы создали незамысловатый "учебный" пакет fqdn.org/common-utils (вспомните, как это было). Не мудрствуя лукаво, добавим в этот пакет новый файл objects.go, дабы было ясно, что его предназначение - хранить именованные типы данного пакета:

Алфавит и пакеты