← На главную

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

Я потихоньку слежу за развитием языка 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.

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