Поиск повторяющихся строк с помощью Go
Программы для копирования файлов, печати, поиска, сортировки, подсчета и другие имеют схожую структуру:
цикл по входным данным, некоторые вычисления над каждым элементом и генерация вывода "на лету" или по окончании вычислений.
Давайте проработем три варианта программы под названием "dup", некоего аналога uniq
из Unix,
которая ищет соседние повторяющиеся строки.
Вариант 1
Здесь "dup" выводит каждую строку, которая в стандартном вводе появляется больше одного раза, выводя предварительно количество ее появлений. В этой программе вводятся:
- инструкция
if
- тип данных
map
- пакет
bufio
// Выводит текст каждой строки, которая появляется // в стандартном вводе более одного раза, // а также количество ее появлений package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } // Примечание: игнорируем потенциальные ошибки из input.Err() for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } }
Отображение map
содержит набор пар "ключ-значение"
и обеспечивает константное время выполнения операций хранения, извлечения или проверки наличия элемента в множестве.
Если провести аналогию с языком программирования php, то map
в Golang - это ассоциативный массив из php.
Но в Go и ключи, и значения map
должнв быть одного и того же типа.
Ключ может быть любого типа, лишь бы значения этого типа можно было сравнить с помощью оператора ==; распространенным примером ключа являются строки.
Значение может быть любого типа. В нашем примере ключи представляют собой строки, а значения представлены типом int
.
Встроенная функция make
создает новое пустое отображение, хотя она имеет и другие применения.
Всякий раз, когда "dup" считывает строку ввода, эта строка используется как ключ в отображении, и соответствующее значение увеличивается.
Инструкция counts[input.Text()]++
эквивалентна следующим двум инструкциям:
- line := input.Text()
- counts[line] = counts[line] + 1
Если в отображении еще нет нужного нам ключа, это не проблема.
Когда новая строка встречается впервые, выражение counts[line]
в правой части
присваивает нулевое значение новому элементу, которое для типа int равно 0.
Для вывода результатов мы вновь используем цикл по диапазону, на этот раз — по отображению counts.
Как и ранее, каждая итерация дает две величины
- ключ и
- значение элемента отображения для этого ключа.
Порядок обхода отображения не определен, на практике этот порядок случаен и варьируется от одного выполнения программы к другому.
Это сделано преднамеренно, поскольку предотвращает написание программ, опирающихся на конкретное упорядочение, которое не гарантируется.
Далее наступает очередь пакета bufio
, который помогает сделать ввод и вывод эффективным и удобным.
Одной из наиболее полезных его возможностей является тип с именем Scanner,
который считывает входные данные и разбивает их на строки или слова;
это самый простой способ обработки ввода, который поступает построчно.
Программа использует краткое объявление переменной для создания новой переменной input
,
которая ссылается на bufio.Scanner
:
input := bufio.NewScanner(os.Stdin)
Сканер считывает стандартный ввод программы.
Каждый вызов input.Scan()
считывает очередную строку и удаляет завершающий символ новой строки;
результат можно получить путем вызова input.Text()
.
Функция Scan возвращает значение true, если строка считана и доступна,
и значение false, если входные данные исчерпаны.
Функция fmt.Printf
выполняет форматированный вывод на основе списка выражений.
Первым ее аргументом является строка формата, которая указывает, как должны быть отформатированы последующие аргументы
.
Формат каждого аргумента определяется символом преобразования, буквой, следующей за знаком процента.
Строка формата содержит также символы табуляции \t
и новой строки \n
.
Строковые литералы могут содержать такие управляющие последовательности для представления символов,
которые обычно невидимы на экране и не могут быть введены непосредственно.
По умолчанию Printf не записывает символ новой строки.
По соглашению функции форматирования, имена которых заканчиваются на f,
такие как log.Printf
и fmt.Errorf
, используют правила форматирования fmt.Printf
,
тогда как функции, имена которых заканчиваются на ln
, как Println,
форматируют свои аргументы так, как будто используется символ преобразования %v
, а за ним — символ новой строки.
Вариант 2
Многие программы считывают входные данные либо из стандартного ввода, как приведенная выше, либо из последовательности именованных файлов.
Следующая версия "dup" может как выполнять чтение стандартного ввода, так и работать со списком файлов, используя os.Open
для их открывания:
// Выводит текст каждой строки, которая появляется // во входных данных более одного раза. // Программа читает стандартный ввод или список именованных файлов package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, "dup2: %v\n", err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } } func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } // Примечание: игнорируем потенциальные ошибки из input.Err() }
Функция os.Open
возвращает два значения.
Первое из них является открытым файлом (*os. File)
, который в дальнейшем читает Scanner.
Второй результат — значение встроенного типа error
.
Если err
равно специальному встроенному значению nil
, файл открыт успешно.
Этот файл читается, и по достижении его конца функция Close закрывает его и освобождает все связанные с ним ресурсы.
С другой стороны, если значение err
не равно nil
, значит, что-то пошло не так.
В этом случае значение ошибки описывает происшедшую неприятность.
Наша простейшая обработка ошибок выводит сообщение в стандартный поток сообщения об ошибках
с помощью вызова Fprintf
и символов преобразования %v (которые выводят значение любого типа в формате по умолчанию),
после чего "dup" переходит к следующему файлу;
инструкция continue
выполняет немедленный переход к очередной итерации охватывающего цикла for.
Обратите внимание, что вызов countLines
предшествует объявлению этой функции.
Функции и другие объекты уровня пакета могут быть объявлены в любом порядке.
Отображение является ссылкой на структуру данных, созданную функцией make
.
Когда отображение передается в функцию, функция получает копию ссылки,
поэтому любые изменения, которые вызываемая функция вносит в указываемую структуру данных,
будут видны и с помощью исходной ссылки в вызывающем коде.
В нашем примере значения, вставляемые в отображение counts функцией countLines, видны функции main.
Вариант 3
Рассмотренные выше варианты приложения "dup" работают в "потоковом" режиме, в котором входные данные считываются и разбиваются на строки по необходимости, так что в принципе эти программы могут обрабатывать произвольное количество входных данных. Альтернативный подход состоит в чтении всех входных данных в память одним большим "глотком", полном разделении его на строки, и последующей обработке строк. 3-й вариант нашей программы работает именно таким образом.
В ней вводятся функция ReadFile
(из пакета io/ioutil
),
которая считывает все содержимое именованного файла, и функция strings.Split
,
которая разбивает строку на срез подстрок.
Как вы уже поняли, этот вариант программы несколько упрощен.
Во-первых, этот вариант программы читает только именованные файлы,
но не стандартный ввод, так как функции ReadFile
требуется аргумент, который представляет собой имя файла.
Во-вторых, мы перенесли подсчет строк обратно в функцию main
, так как теперь он необходим только в одном месте.
package main import ( "fmt" "io/ioutil" "os" "strings" ) func main() { counts := make(map[string]int) for _, filename := range os.Args[1:] { data, err := ioutil.ReadFile(filename) if err != nil { fmt.Fprintf(os.Stderr, "dup3: %v\n", err) continue } for _, line := range strings.Split(string(data), "\n") { counts[line]++ } } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } }
Функция ReadFile
возвращает байтовый срез,
который должен быть преобразован в string
так, чтобы его можно было разбить с помощью функции strings.Split
.
Внутри функции bufio.Scanner, ioutil.ReadFile и ioutil.WriteFile используют методы Read и Write объекта *os. File, но большинству программистов очень редко приходится прибегать к таким низкоуровневым функциям непосредственно. Проще использовать функции более высокого уровня наподобие функций из пакетов:
- bufio
- io/ioutil