CoderCastrov logo
CoderCastrov
Go

Использование Golang для взаимодействия с REST API

Использование Golang для взаимодействия с REST API
просмотров
6 мин чтение
#Go

Я стал довольно зависимым от Golang за последние несколько месяцев, и я думаю, что нет лучшего приложения, чтобы показать, почему, чем то, как вы можете взаимодействовать с REST API с помощью Golang. В этой статье я буду использовать API DigitalOcean с некоторыми тестовыми данными, которые у меня есть на моем аккаунте DigitalOcean. Если вы будете следовать за мной, вы не получите те же результаты, что и я, и вам придется создать одну или две виртуальные машины на своем собственном аккаунте, чтобы увидеть результаты.

Давайте начнем!

Базовая настройка

Я создал новый ключ API DigitalOcean на своем аккаунте для демонстрационных целей и поместил его в файл .env. Мы загрузим эту переменную .env в начале нашей функции main:

func main() {
	err := godotenv.Load()
	if err != nil {
		fmt.Errorf("There was an error loading the dotenv file: %s\n", err.Error())
	}
	DOAPIKEY := os.Getenv("DOAPIKEY")
}

Теперь, когда у нас загружен ключ API, давайте создадим основной URL-путь, который мы хотим использовать для получения списка всех наших виртуальных машин. Если мы посмотрим на документацию API DigitalOcean, мы увидим, что он находится по пути /v2/droplets/. Мы будем использовать конструктор URL для создания этого пути вместе с константой BASEURL.

const BASEURL = "https://api.digitalocean.com"
func main() {
	err := godotenv.Load()
	if err != nil {
		fmt.Errorf("There was an error loading the dotenv file: %s\n", err.Error())
	}
	DOAPIKEY := os.Getenv("DOAPIKEY")	
	dropleturl, err := url.JoinPath(BASEURL, "/v2/droplets")
	if err != nil {
		fmt.Errorf("There was an error trying to create the droplet URL: %s\n", err.Error())
	}
}

Теперь, когда у нас есть URL и наш ключ API, мы можем создать функцию makeReq(), которая будет возвращать правильно отформатированный объект http.Request{} с заголовками аутентификации и установленным методом запроса.

func makeReq(method string, apikey string, url string) (*http.Request, error) {
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return &http.Request{}, err
	}
	req.Header.Add("Authorization", "Bearer "+apikey)
	req.Header.Add("Content-Type", "application/json")
	return req, nil
}

Этот запрос будет GET-запросом, поэтому, когда мы вызываем функцию для получения отформатированного запроса, мы передаем "GET" в качестве метода, а затем передаем отформатированный объект http.Request{} клиенту HTTP для его обработки.

func main() {
	err := godotenv.Load()
	if err != nil {
		fmt.Errorf("There was an error loading the dotenv file: %s\n", err.Error())
	}
	DOAPIKEY := os.Getenv("DOAPIKEY")

	dropleturl, err := url.JoinPath(BASEURL, "/v2/droplets")
	if err != nil {
		fmt.Errorf("There was an error trying to create the droplet URL: %s\n", err.Error())
	}
	fmt.Println(dropleturl)
	req, err := makeReq("GET", DOAPIKEY, dropleturl)
	client := http.Client{}
	res, err := client.Do(req)
	if err != nil {
		fmt.Errorf("There was an error doing the request: %s", err.Error())
	}
}

Теперь вот где GoLang становится потрясающим...

Приведение типов и разбор данных

После выполнения запроса мы получаем объект http.Response{}, который нам не особо полезен. Мы можем получить тело ответа через res.Body с помощью метода ioutil.ReadAll(), но это просто массив байтов. Мы можем преобразовать этот массив байтов в строку, но тогда у нас будет просто большая неприятная строка JSON, которая снова не очень полезна.

Способ, которым GoLang обрабатывает это, - это красиво. Вы создаете структуры с форматом ваших типов данных, описанных как члены структуры. Каждый член будет иметь тег, который сообщает GoLang, как разобрать JSON-данные в эту структуру. Затем вы просто разбираете массив байтов в ваши собственные структуры и у вас есть красиво разобранные данные.

Если разбор происходит некорректно, ваши поля просто не будут заполнены, и у вас будут нулевые значения в этих полях структуры. Это означает, что если, скажем, у JSON-объекта есть значение, которое может быть задано или может отсутствовать, или поле, которое может присутствовать или может отсутствовать, вы можете определить его в своей структуре, и если его нет, GoLang просто не устанавливает его в объекте.

Давайте покажем пример. Наш ответ от DigitalOcean будет структурирован следующим образом:

{
	"droplets":[
    	{
        	"id":123456,
            "value2":"",
            "value3":"",
            ...
			"image":{},
            ...
        }
    ]
}

По сути, у вас будет массив droplets, который будет содержать 0 или более элементов droplet, каждый из которых является блобом строк, целых чисел и вложенных объектов. Критически важно, что нам не нужно заботиться о каждом поле в объекте! Только о тех, которые нас интересуют. В этом примере я хочу извлечь поле name, поле id, поле created_at и поле id и name изображения, связанного с droplet.

Сначала я создам структуру, цель которой - разобрать массив droplets.

type Droplets struct {
	Drops []Droplet `json:"droplets"`
}

type Droplets struct говорит GoLang, что мы создаем структуру с именем Droplets. Затем мы определяем поле с именем Drops и объявляем его как массив типа Droplet, который является структурой, которую мы определим ниже. Часть между обратными кавычками, json:"droplets", - это тег, который говорит GoLang "Если вы пытаетесь разобрать JSON-блоб в эту структуру, поле Drops будет определено в поле droplets в JSON-блобе".

Затем мы создаем нашу структуру Droplet.

type Droplet struct {
	Id        int    `json:"id"`
	Name      string `json:"name"`
	CreatedAt string `json:"created_at"`
	Image     Img    `json:"image"`
}

Все это то же самое, что и выше, за исключением поля Image. У вас есть имя поля, такое как Id, Name и т. д., типы этих полей и тег для разбора JSON из полей в JSON-блобе. Поле Image имеет тип Img, который является другой пользовательской структурой, определенной следующим образом:

type Img struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
}

Таким образом, чтобы получить доступ к идентификатору изображения в объекте Droplet с именем drop, мы запустим:

fmt.Println(drop.Img.Id)

Теперь давайте разберем все наши droplet, возвращенные из API DigitalOcean. Сначала мы создадим пустой объект Droplets. Затем мы разберем (или разберем) наше тело ответа в этот объект Droplets. Наконец, мы пройдем циклом по массиву droplet и выведем информацию.

func main() {
	err := godotenv.Load()
	if err != nil {
		fmt.Errorf("There was an error loading the dotenv file: %s\n", err.Error())
	}
	DOAPIKEY := os.Getenv("DOAPIKEY") 
        dropleturl, err := url.JoinPath(BASEURL, "/v2/droplets")
         if err != nil {
           fmt.Errorf("There was an error trying to create the droplet URL: %s\n", err.Error())
         }
         fmt.Println(dropleturl)
         req, err := makeReq("GET", DOAPIKEY, dropleturl)
         client := http.Client{}
         res, err := client.Do(req)
         if err != nil {
          fmt.Errorf("There was an error doing the request: %s", err.Error())
         }
         resbody, err := ioutil.ReadAll(res.Body)
         if err != nil {
          fmt.Errorf("There was an error reading the response body: %s", err.Error())
         }
         //fmt.Println(string(resbody))
         drops := Droplets{}
         err = json.Unmarshal(resbody, &drops)
         if err != nil {
          fmt.Errorf("There was an error unmarshaling the struct: %s", err.Error())
         }
         for _, drop := range drops.Drops {
           fmt.Printf("Droplet ID %d\n\tName: %s\n\tCreated At: %s\n", drop.Id, drop.Name, drop.CreatedAt)
           fmt.Printf("\tImage info:\n\t\tImage ID: %d\n\t\tImage name: %s\n\n", drop.Image.Id, drop.Image.Name)
         }
}

Почему эти правила

GoLang создал, вероятно, наилучший подход к разбору данных JSON. Вы объявляете свои структуры данных таким образом, чтобы они были достаточно строгими для разбора нужных вам данных, но достаточно гибкими для работы со структурами, имеющими гибкие определения полей. Если по какой-либо причине поле name не было установлено в приведенном выше примере для определенного droplet, это не проблема, просто будет выведена пустая строка для поля name. Если поле droplets было бы пустым (как в случае, если у меня не было бы создано ни одного droplet), это не вызвало бы критической ошибки, приводящей к сбою программы, у меня просто был бы пустой массив, и цикл for не начался бы.

Это создает потрясающий опыт разработчика для разбора данных REST API и является основной причиной, по которой я теперь придерживаюсь GoLang.