Многопоточный генератор шоунотов на Go

10 августа 2015

Я потихоньку слежу за развитием языка Go и в последнее время он начинает нравится мне все больше и больше. Недавно дошли руки написать небольшую программку, генерирующую шоуноты к подкасту по списку ссылок. Ну и, поскольку это все-таки Go, программка была сделана многопоточной.

Напоминаю, что ранее в этом блоге публиковались аналогичные программки на Kotlin и Rust. Так что, не буду в очередной раз расписывать, что, как и зачем, а просто приведу код:

package main

import (
  "fmt"
  utf8string "golang.org/x/exp/utf8string"
  "io/ioutil"
  "net/http"
  "os"
  "regexp"
  "runtime"
  "strings"
)

type task struct {
  num int
  url string
}

type result struct {
  num   int
  url   string
  title string
}

func formatTitle(title string) string {
  title = strings.TrimSpace(title)
  utf8title := utf8string.NewString(title)

  // or: utf8.RuneCountInString(title)
  if utf8title.RuneCount() > 64 {
    return fmt.Sprint(utf8title.Slice(0, 64), "...")
  } else {
    return title
  }
}

func getTitle(url string) string {
  req, reqErr := http.NewRequest("GET", url, nil)
  if reqErr != nil {
    return fmt.Sprint("reqErr: ", reqErr.Error())
  }
  req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) " +
  "Apple WebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 " +
  "Safari/537.36")

  // resp, getErr := http.Get(url)

  client := http.Client{}
  resp, getErr := client.Do(req)
  if getErr != nil {
    return fmt.Sprint("getErr: ", getErr.Error())
  }
  defer resp.Body.Close()

  bytes, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    return err.Error()
  }

  content := string(bytes)
  r, _ := regexp.Compile("(?is)<title[^>]*>(.*?)</title>")
  matches := r.FindStringSubmatch(content)
  if len(matches) > 1 {
    return formatTitle(matches[1])
  } else {
    return "[NO TITLE]"
  }
}

func worker(workerId int, tasksChan <-chan task,
            resultsChan chan<- result) {
  for {
    tsk := <-tasksChan
    fmt.Printf("Worker %d - processing %s...\n", workerId, tsk.url)

    rslt := result {
      num:    tsk.num,
      title:  getTitle(tsk.url),
      url:    tsk.url,
    }
    resultsChan <- rslt
  }
}

func main() {
  if len(os.Args) < 2 {
    fmt.Fprintf(os.Stderr, "Usage: %s <infile>\n", os.Args[0])
    os.Exit(1)
  }

  bytes, ferr := ioutil.ReadFile(os.Args[1])
  if ferr != nil {
    panic(ferr)
  }

  fileContent := string(bytes)

  r, _ := regexp.Compile("(?i)https?://[^\\s]+")
  urls := r.FindAllString(fileContent, 1000)
  urlsNumber := len(urls)

  tasksChan := make(chan task, urlsNumber)
  resultsChan := make(chan result, urlsNumber)

  cpuNum := runtime.NumCPU()
  for i := 0; i < cpuNum; i++ {
    go worker(i, tasksChan, resultsChan)
  }

  for idx, url := range urls {
    tsk := task{ num: idx, url: url }
    tasksChan <- tsk
  }

  results := make([]result, urlsNumber)
  for i := 0; i < urlsNumber; i++ {
    res := <-resultsChan
    results[res.num] = res
  }

  fmt.Print("<ul>\n")
  for i := 0; i < urlsNumber; i++ {
    fmt.Printf("<li><a href=\"%s\">%s</a></li>\n",
               results[i].url, results[i].title)
  }
  fmt.Print("</ul>\n")
}

Пара моментов, на которые следует обратить внимание:

  • Для правильного определения длины юникодной строки и взятия подстроки следует использовать пакет exp/utf8string;
  • Размер буфера у каналов tasksChan и resultsChan устанавливается в urlsNumber. Если этого не сделать, главная горутина запишет cpuNum сообщений в tasksChan, после чего произойдет дэдлок — воркеры не начнут читать новые задачи из tasksChan, пока главная горутина не начнет забирать результаты из resultsChan. Прямо-таки встроенный механизм бэкпреше по всей программе;
  • Программа завершается при завершении главной горутины, поэтому нет необходимости как-то вручную останавливать воркеров;
  • Есть серьезные подозрения, что приведенный код очень неоптимален. Насколько я понимаю, контент всех загружаемых страниц лишний раз копируется при приведении []byte в string и держится в памяти до вывода title. В небольшой консольной утилите это, пожалуй, еще простительно, но долгоживущий серверный код так писать нельзя;
  • Следите, что вы передаете через каналы. Сообщения передаются по значению, но при этом не делается их глубокая копия. Поэтому, если в сообщениях есть ссылки, два потока могут получить доступ к одним и тем же данным;

В целом, пока что у меня исключительно положительные впечатления от языка. Пишется на Go легко и приятно. Поддержка в IntelliJ IDEA почти нормальная. Через Google без труда находится документация и куча ответов на StackOverflow. Особенно пригодились сайты gobyexample.com и godoc.org. Много инсайтов можно найти в блоге research.swtch.com.

Что бы еще такого на нем писать?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.