Однією з незвичних особливостей Go є те, що функції та методи можуть повертати декілька значень водночас. Ця форма може бути використана для покращення кількох незграбних ідіом у програмах на C: повернення помилок накшталт -1 для EOF та модифікація аргументу, переданого за адресою.
У мові C помилка запису сигналізується за допомогою негативного лічильника, а код помилки зберігається в нестабільній локації. У Go метод Write
може одночасно повернути й лічильник, і помилку: «Так, ви записали деякі байти, але не всі, оскільки заповнили пристрій». Сигнатура методу Write
для файлів з пакета os
має вигляд:
func (file *File) Write(b []byte) (n int, err error)
і, як зазначено у документації, даний метод повертає кількість записаних байт і ненульову помилку, якщо n != len(b)
. Це поширений стиль; див. розділ про обробку помилок для отримання додаткових прикладів.
Подібний підхід позбавляє від необхідності передавати вказівник на значення, що повертається, для імітації параметра-посилання. Ось проста функція для отримання числа з позиції у зрізі байт, яка повертає це число і наступну позицію.
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ { }
x = x*10 + int(b[i]) - '0'
}
return x, i
}
Ви можете використовувати його для сканування чисел у вхідному зрізі b
таким чином:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
У Go параметрам повернення або «параметрам результату» функції можна присвоювати імена й використовувати їх як звичайні змінні, так само як і вхідні параметри. Якщо параметрам присвоєно імена, вони ініціалізуються нульовими значеннями для своїх типів на початку роботи функції; якщо функція виконує оператор return
без аргументів, то як значення, що повертаються, використовуються поточні значення параметрів результату.
Імена не є обов'язковими, але вони можуть зробити код коротшим і зрозумілішим: це документація. Якщо ми дамо імена результатам функції nextInt
, то стане зрозуміло, який саме int
повертається.
func nextInt(b []byte, pos int) (value, nextPos int) {
Оскільки іменовані результати ініціалізуються і прив'язуються до повернення, вони можуть спростити та прояснити обробку та розуміння значень, що повертаються з функції. Ось версія io.ReadFull
, яка добре їх використовує:
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
Оператор defer
у Go планує виклик функції (відкладеної функції) на виконання безпосередньо перед тим, як функція, що виконує відкладення, повернеться. Це незвичний, але ефективний спосіб впоратися з такими ситуаціями, як ресурси, які необхідно звільнити незалежно від того, яким шляхом повернеться функція. Канонічними прикладами є розблокування м'ютексу або закриття файлу.
// Contents повертає вміст файлу у вигляді рядка.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close буде виконано, коли ми завершимо.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append буде обговорено пізніше.
if err != nil {
if err == io.EOF {
break
}
return "", err // f.Close буде виконано, якщо ми повернемося тут.
}
}
return string(result), nil // f.Close буде виконано, якщо ми повернемося тут.
}
Відкладання виклику таких функцій, як Close
, має дві переваги. По-перше, це гарантує, що ви ніколи не забудете закрити файл - помилка, якої легко припуститися, якщо пізніше відредагувати функцію і додати новий шлях повернення. По-друге, це означає, що close
знаходиться поруч з open
, що набагато зрозуміліше, ніж розміщувати його в кінці функції.
Аргументи відкладеної функції (які включають приймач, якщо функція є методом) обчислюються під час виконання defer
, а не під час виконання функції безпосередньо. Крім того, що ви можете не турбуватися про зміну значень змінних під час виконання функції, це означає, що одне місце виклику defer
може відкласти виконання декількох функцій. Ось дурненький приклад:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Відкладені функції виконуються у порядку LIFO (англ. last in, first out, «останнім прийшов — першим пішов»), тому цей код призведе до виведення 4 3 2 1 0
, коли функція повернеться. Більш правдоподібним прикладом є простий спосіб відстежити виконання функції у програмі. Ми можемо написати декілька простих підпрограм трасування на зразок наступної:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
// Використовуйте їх так:
func a() {
trace("a")
defer untrace("a")
// якась робота...
}
Ми можемо зробити краще, використовуючи той факт, що аргументи відкладених функцій обчислюються під час виконання відкладання defer
. Процедура трасування може встановити аргумент для процедури зняття трасування. Маємо код,
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
результатом виконання якого буде
entering: b
in b
entering: a
in a
leaving: a
leaving: b
Для програмістів, які звикли до управління ресурсами на рівні блоків з інших мов, defer
може здатися незвичним, але його найцікавіші та найпотужніші застосування пов'язані саме з тим, що він базується не на блоках, а на функціях. У розділі про помилки ми побачимо ще один приклад можливостей цього оператора.