Многопоточный генератор шоунотов на Go
10 августа 2015
Я потихоньку слежу за развитием языка Go и в последнее время он начинает нравится мне все больше и больше. Недавно дошли руки написать небольшую программку, генерирующую шоуноты к подкасту по списку ссылок. Ну и, поскольку это все-таки Go, программка была сделана многопоточной.
Напоминаю, что ранее в этом блоге публиковались аналогичные программки на Kotlin и Rust. Так что, не буду в очередной раз расписывать, что, как и зачем, а просто приведу код:
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.
Что бы еще такого на нем писать?
Метки: Go, Параллелизм и многопоточность.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.