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

Was this helpful?

Функции

Яблоки - это блоки, из которых состоит Я

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

Дефиниция

Что в программировании называют функцией? Вовсе не пустой вопрос. В математике, к примеру, функцией называют в широком смысле зависимость некоторых значений от некоторых аргументов. В программировании же функции - это логически завершённые блоки кода, выполняющие определённую работу. Они используются в первую очередь с целью избежания повторения однотипного кода в теле программы. Перечень переменных и их типы, которые принимает функция называются параметрами функции. Значения, присваиваемые параметрам, называются аргументами. Имена параметров и аргументов могут не совпадать, главное - это соблюдение типа. При этом функция может вовсе не использовать аргументов и не возвращать никаких значений. Сумма таких понятий, как имя функции, перечень её параметров и возвращаемых значений называется сигнатурой функции. В связи с тем, что в некоторых языках программирования существует так называемый полиморфизм функций, определение сигнатуры может в определённых случаях отличаться от приведенного выше. Таким образом функции - это в первую очередь элементы хорошо структурированного кода. Необходимо понимать, что большинство задач в вашей программе может быть выполнено и без применения хотя бы единственной функции. Однако, непременным свойством хорошего кода является выделение логически завершённых блоков и оформление их в виде функций. Это сродни хорошей литературе - когда мы сперва ясно осознаём и формулируем свои мысли, а затем складываем их в абзацы и предложения, придавая тексту структуру.

Между прочим, мы уже имели возможность косвенно познакомиться по крайней мере с одной функцией! Эта функция - main - точка входа в каждую нашу программу.

func main() {
}

Особенности

Какие особенности присущи функциям в языке Go? Таких особенностей две. Первая из них - это возможность возвращения не одного, а нескольких значений, не прибегая к дополнительным инструментам, наподобие модификаторов параметров out в C#. Вторая особенность - это существование так называемых вариативных функций, то есть таких, которые могут принимать неопределённое число параметров. Но давайте обо всём по порядку. От простого - к сложному.

Простейшая функция - без параметров и возвращаемого значения.

Первое, что стоит рассмотреть, это простейшая функция, которая не имеет ни параметров, ни возвращаемых значений. Синтаксис такой функции будет состоять из ключевого слова func, имени функции, круглых скобок () и тела функции в фигурных скобках {}.

func nameOfFunc(){ // открывающая скобка в той же строке!
}

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

package main

import (
	"fmt"
)

func printPifagorTable() {
	for i := 1; i < 10; i++ {
		for j := 1; j < 10; j++ {
			fmt.Print(i*j, "\t")
		}
		fmt.Println()
	}

}

func main() {
	printPifagorTable()

}
//Результат:
/*
1		2		3		4		5		6		7		8		9	
2		4		6		8		10	12	14	16	18	
3		6		9		12	15	18	21	24	27	
4		8		12	16	20	24	28	32	36	
5		10	15	20	25	30	35	40	45	
6		12	18	24	30	36	42	48	54	
7		14	21	28	35	42	49	56	63	
8		16	24	32	40	48	56	64	72	
9		18	27	36	45	54	63	72	81
*/

Нередко это бывает циклически выводимое меню:

package main

import (
	"fmt"
)

func printMenu() {
	fmt.Println("Press a key to say:")
	m := map[int]string{
		1: "Hello!",
		2: "How do you do?",
		3: "Exit",
	}

	for i := 1; i <= len(m); i++ {
		fmt.Println(i, m[i])
	}

}

func main() {
	var input string
	for {
		printMenu()
		fmt.Scan(&input)
		switch input {
		case "1":
			fmt.Println("Hello!")
		case "2":
			fmt.Println("I`m great!")
		case "3":
			fmt.Println("Bye!")
		}
		if input == "3" {
			break
		}

	}
}
//Результат:
/*
Press a key to say:
1 Hello!
2 How do you do?
3 Exit
1
Hello!
Press a key to say:
1 Hello!
2 How do you do?
3 Exit
2
I`m great!
Press a key to say:
1 Hello!
2 How do you do?
3 Exit
3
Bye!
*/

Функция, принимающая параметры и возвращающая значения

Синтаксически, в круглых скобках здесь добавится один или несколько параметров. После круглых скобок последует возвращаемое значение. Наличие возвращаемого значения подразумевает введение оператора return, которым должны завершиться все ветви кода в теле функции.

func nameOfFunc (i int, f float64) bool {
// some code here
return true
}

Если параметры однотипны, их можно перечислить через запятую, вот так:

func nameOfFunc (a, b int, f float64) bool {
// some code here
return true
}

Если возвращаемых значений несколько, они, как и параметры, берутся в круглые скобки. На практике, чаще всего, вторым параметром функция возвращает ошибку:

func nameOfFunc (a, b int, f float64) (float64, error) {
// some code here
return 0.01, nil
}

Приведём пример. Пускай наша функция производит деление с защитой от деления на 0:

package main

import (
	"errors"
	"fmt"
)

func division(x, y float64) (float64, error) {
	var err error
	if y == 0 {
		err = errors.New("division by zero error")
		return 0, err
	}

	return x / y, nil

}

func main() {
	fmt.Println(division(12, 5))
	fmt.Println(division(5, 0))
}
//Результат:
//2.4 <nil>
//0 division by zero error

Зачастую для обработки ошибок пишут отдельную функцию, чтобы избежать характерных для Go повторений блоков кода if err != nil { //..... Вот пример такой функции-обёртки:

func failOnError(err error, msg string) {
  if err != nil {
    log.Fatalf("%s: %s", msg, err) // выход из программы при ошибке!
  }
}

Возвращаемые значения могут быть именованными. Синтаксически это выглядит так:

func nameOfFunc (a, b int, f float64) (result float64, err error) {
// some code here
return result, err
}

Изменим соответствующим образом функцию деления:

package main

import (
	"errors"
	"fmt"
)

func division(x, y float64) (result float64, err error) {
	if y == 0 {
		err = errors.New("division by zero error")
		return result, err
	}

	result = x / y

	return result, err

}

func main() {

	fmt.Println(division(12, 5))
	fmt.Println(division(5, 0))

}
//Результат:
//2.4 <nil>
//0 division by zero error

Использование именованных возвращаемых значений имеет определённый конкретный смысл и эффект. Будучи именованными, эти переменные изначально инициализируются значениями типа по умолчанию и сразу доступны в теле функции. Таким образом, нет необходимости объявлять дополнительные переменные. Но самый главный эффект проявляется в том случае, если нам нужно восстановиться после ошибки и всё же вернуть из функции некоторые значения. Об этом сейчас и поговорим.

Defer

В переводе с английского defer означает откладывать, отсрочивать. Ожидаемо, функции, объявленные с этим ключевым словом становятся отложенными. такая функция выполняется после выполнения всего следующего за ней блока кода. Например:

package main

import (
	"fmt"
)

func helloFriends() {
	fmt.Println("Hello, friends!")
}

func main() {
	defer helloFriends()
	fmt.Println("Hello, world!")
}
//Результат:
//Hello, world!
//Hello, friends!

Таких функций может быть несколько, тогда они выполняются по принципу "всплывания" снизу вверх (LIFO - last in, first out):

package main

import (
	"fmt"
)

func deferFunc(s string) {
	fmt.Println(s)
}

func main() {
	defer deferFunc("In 1")
	defer deferFunc("In 2")
	defer deferFunc("In 3")

	fmt.Println("Hello, world!")
}
//Результат:
//Hello, world!
//In 3
//In 2
//In 1

Основных предназначений у этого приёма программирования - два. Первое, это "брошенное" закрытие открытых соединений:

package main

import (
	"log"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:80") // open connection
	if err != nil {
		log.Fatalf("net.Dial: %v", err)
	}
	defer conn.Close() // shure to close it later
}

Второе же применение - восстановление (recover) после ошибки (exception). Приведём пример восстановления после ошибки, с возвращением параметров:

package main

import (
	"fmt"
)

type commonResponse struct {
	status string
	result int
	error  bool
}

func printSliceElement(n int, slice []int) (c commonResponse) {

	defer func() { //анонимная функция, есть и такое!
		if r := recover(); r != nil {
			fmt.Println("Recovered in printSliceElement func, continue: ", r)
			c.status = "Failure"
		}

	}()

	c.status = "Succsess"
	c.result = slice[n]
	c.error = false

	return c

}

func main() {

	slice := []int{0, 1, 2, 3, 4}
	c := printSliceElement(4, slice)
	fmt.Println(c)
	c = printSliceElement(5, slice)
	fmt.Println(c)

}
//Результат:
//{Succsess 4 false}
//Recovered in printSliceElement func, continue:  runtime error: index out of range [5] with length 5
//{Failure 0 false}

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

Анонимные функции

В примере с восстановлением после критической ошибки внимательный читатель, безусловно, обнаружил новую для себя конструкцию:

func(){
// do something here
}()

Что же, поздравляю, вы были весьма проницательны! У нас новая "нозологическая единица". Это - анонимная функция. Круглые скобки в конце означают выполнение кода "на месте". В простейшем виде выглядеть это будет так:

package main

import (
	"fmt"
)

func main() {

	func() {
		fmt.Println("Anonymous function!")
	}()
}
//Результат:
//Anonymous function!

Чтобы сделать функцию более функциональной (масло масляное), в скобки можно поместить параметры:

package main

import (
	"fmt"
)

func main() {

	res := func(i int) int {
		return i * i
	}(2)
	fmt.Println(res)
}
//Результат:
//4

Можно создать переменную - функцию:

package main

import (
	"fmt"
)

var f = func(i int) int {
		return i * i
	}

func main() {
	
	fmt.Println(f(2))
}
//Результат:
//4

Нередко анонимные функции используются как go-подпрограммы (goroutines, горутины). Но об этом следует поговорить несколько позже, в разделе о многопоточности.

Значения-функции

В последнем примере переменная f - представляет собой так называемое значение-функцию. Таким образом, функции (анонимные или нет), могут быть присвоены переменным. А ещё они могут быть, к примеру, переданы в другую функцию и возвращены из неё. Если функции возвращают функции, то они выстраиваются в так называемый «стек вызовов» по типу FIFO. Посмотрим, что из себя представляет переменная f:

package main

import (
	"fmt"
)

var f = func(i int) int {
	return i * i
}

func main() {

	fmt.Printf("%T", f)
}
//Результат:
//func(int) int

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

package main

import (
	"fmt"
)

var f = func(i int) int {
	return i * i
}

func voidFunc() {
}

func main() {

	fmt.Printf("%T", f)
	f = voidFunc
}
//Результат:
//ошибка компиляции!
//cannot use voidFunc (type func()) as type func(int) int in assignment

Нулевым значением типа функции является nil. Вызов нулевой функции приводит к аварийной ситуации. Значения-функции не являются сравниваемыми.

Рекурсия

Вероятно, это одна из самых нелюбимых и запутанных тем, относительно разбора предмета "функции". Чаще всего, от использования рекурсии можно безболезненно воздержаться, что не может не радовать. Однако, существуют классические алгоритмы, которые решаются именно применением рекурсии, и поэтому я не вижу причин не применять её, если контекст программы соответствует этому. Что есть рекурсивная функция? Это функция, которая в процессе работы вызывает сама себя. Не будем углубляться здесь в академические дебри обработки рекурсивных структур данных. Ограничимся классическим примером рекурсии - вычислением факториала:

package main

import (
	"fmt"
)

func factorial(n uint) uint {
	if n == 0 {
		return 1
	}
	return n * factorial(n-1)
}

func main() {

	fmt.Println(factorial(5))
}
//Результат:
//120

Замыкания

Функцию, использующую переменные, определенные вне этой функции, называют замыканием. Замыкания и рекурсия считаются основными приёмами так называемого функционального программирования. Приведём простой пример:

package main

import (
	"fmt"
)

var flag bool // глобальная переменная

func setFlagOn() {
	flag = true
}

func setFlagOff() {
	flag = false
}

func main() {
	setFlagOn()
	fmt.Println(flag)
	setFlagOff()
	fmt.Println(flag)

}
//Результат:
//true
//false

Замыканием также является функция, возвращающая другую функцию:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func makeRandom() func() int {
	min := 0
	max := 100
	return func() (ret int) {
		rand.Seed(time.Now().Unix())
		ret = rand.Intn(max-min) + min
		return
	}
}

func main() {
	random := makeRandom()
	fmt.Println(random())

}
//Результат:
//рандомное целое число в диапазоне 0...100

Вариативные функции

В числе особенностей функций в Go мы упоминали так называемую вариативность. Рассмотрим это явление поближе. Вариативность функции - это возможность принимать переменное количество однотипных параметров. Синтаксически это выражается многоточием (...) перед указанием типа:

func f (i ...int){
}

Количество передаваемых параметров может быть от нуля (ничего не передаём) и до некоего множества. Важно помнить, что вариативным может быть только единственный параметр, либо же последний в перечне, если параметров несколько:

func f (i ...int){ // хорошо
}

func f1 (b bool, i ...int){ // хорошо
}

func f2 (i ...int, b bool){ // ошибка 
}
//syntax error: cannot use ... with non-final parameter i

На деле тип вариативного параметра - это срез! Убедимся в этом:

package main

import (
	"fmt"
)

func f(i ...int) {
	fmt.Printf("%T\n", i) //получить тип
	fmt.Println(len(i))
}

func main() {

	f()
	f(3, 5, 7)

}
//Результат:
//[]int
//0
//[]int
//3

Возможность не передавать функции аргумент - бесценна. Она позволяет реализовать такую вещь, как параметры по умолчанию. Создадим, к примеру, функцию, которая будет производить паузу:

package main

import (
	"fmt"
	"time"
)

func sleep(duration ...int) {

	var timeout int

	if len(duration) == 0 {
		fmt.Println("No input, using default parameters.")
		timeout = 1
	} else {
		timeout = duration[0]
		fmt.Println("Input:", timeout)
	}

	fmt.Println("Waiting", timeout, "sec.")

	time.Sleep(time.Second * time.Duration(timeout))

}

func main() {

	sleep()
	sleep(5)

}
//Результат:
//No input, using default parameters.
//Waiting 1 sec.
//Input: 5
//Waiting 5 sec.

Иногда случается так, что мы ожидаем чтения данных, а данных не было. В таком случае нам также очень пригодится возможность опционально пропустить передачу аргумента соответствующему параметру.

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

package main

import (
	"fmt"
)

//будем складывать числа
func sum(numbers ...int) int {
	var i int = 0

	for _, val := range numbers {
		i += val
	}
	return i

}

func main() {

	fmt.Println(sum())
	fmt.Println(sum(2, 3))
	fmt.Println(sum([]int{2, 3})) //ошибка!
	//cannot use []int literal (type []int) as type int in argument to sum
	fmt.Println(sum([]int{2, 3}...)) // верное решение!	

}
//Результат:
//0
//5
//5

На этой ноте мы в общих чертах завершим тему о функциях. В добрый путь!

Раунд!

PreviousУказатели и оператор newNextРабота с параметрами командной строки

Last updated 5 years ago

Was this helpful?