golang

Системные вызовы в go

  • пятница, 26 мая 2023 г. в 00:00:18
https://habr.com/ru/articles/737660/

Всем привет!

Всеми нами любимый docker является абстракцией над операционной системой linux, kubernetes является абстракцией над docker, а openshift - это высокоуровневый дистрибутив kubernetes удобный для пользователя.

Технологии сейчас строятся на фундаменте из открытых программ, а самый очевидный путь их использования - системные вызовы из ваших собственных программ.

Все современные языки программирования предоставляют различные интерфейсы для совершения системных вызовов, на мой взгляд одна из самых удачных реализаций представлена в языке go.

Для примера - в установщике openshift installer, данный интерфейс используется 450 раз. Стоит разобрать эту конструкцию, так как она позволяет сильно упростить многие процедуры за счет переиспользования существующих утилит, что бы не "создавать велосипеды" на ровном месте.

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

Для примера я сделал библиотеку, предоставляющую интерфейс к пакетному менеджеру arch linux - pacman.

Проверка зависимостей

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

Эту функцию лучше вызывать на этапе инициализации или в начале main, что бы избежать непредвиденных ошибок в рантайме.

package main

import (
	"fmt"
	"os"
	"os/exec"
	"sync"
)

// Dependecy packages.
const (
	pacman  = `pacman`
)

func init() {
	_, err := exec.LookPath(pacman)
	if err != nil {
		fmt.Println("unable to find pacman in system")
		os.Exit(1)
	}
}

Параметры вызова

Структура exec.Cmd предоставляет большое количество параметров, что бы лучше с ними познакомиться лучше смотреть исходники. Так как статья имеет ознакомительных характер я пройдусь только по основным:

  • Path - это путь к вызываемой программе

  • Args - аргументы используемые при вызове

  • Env - переменные окружения

  • Dir - директория в которой будет исполняться программа

  • Stdin - пользовательский ввод (аналогичным образом с терминалом)

  • Stdout, Stderr - потоки стандартного вывода и ошибок программы

Вот пример вызова знакомой нам утилиты эхо:

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("echo", "hello exec!")
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	fmt.Println(err)
}

Данный код создаст вызов к программе эхо, перенаправит потоки вывода на стандартные утсановленные в системе и запустит процесс.

Если мы хотим прочитать вывод программы и использовать его после, то мы можем воспользоваться буфером из библиотеки bytes и записать вывод программы в него.

package main

import (
	"bytes"
	"fmt"
	"os/exec"
)

func main() {
	cmd := exec.Command("echo", "hello exec!")
	var b bytes.Buffer
	cmd.Stdout = &b
	cmd.Stderr = &b
	cmd.Run()
	fmt.Println(b.String())
}

Так же есть возможность перенаправить вывод программы сразу в несколько различных потоков, сделать это можно используя io.Multiwriter(), вот пример вывода одновременно в stdout и буфер в памяти:

package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
)

func main() {
	cmd := exec.Command("echo", "hello exec!")
	var b bytes.Buffer
	cmd.Stdout = io.MultiWriter(&b, os.Stdout)
	cmd.Run()
	fmt.Println(b.String())
}

Заключение

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

Спасибо!