Obsah
1. Vykreslení tabulek do terminálu v jazyce Go
2. Standardní balíček tabwriter
3. Chování pro záznamy s odlišnou šířkou
4. Specifikace výplňových znaků včetně jejich minimálního počtu
5. Zarovnání hodnot doprava, příznak Debug
7. Zobrazení tabulky s faktoriály hodnot 0 až 20
8. Postupné přidávání hodnot do výsledné tabulky funkcí fmt.Fprintf
10. HTTP server, který nabízí klientům naformátovanou tabulku
11. Balíček olekukonko/tablewriter
12. Vykreslení tabulky s hlavičkou i s okraji buněk
14. Tabulka s patičkou (suma hodnot atd.)
15. Zobrazení tabulky s faktoriály hodnot 0 až 20
16. Výstup do souboru, HTTP server nabízející klientům naformátovanou tabulku
17. Balíček lensesio/tableprinter
18. Ukázky použití balíčku lensesio/tableprinter
19. Repositář s demonstračními příklady
1. Vykreslení tabulek do terminálu v jazyce Go
V poměrně mnoha aplikacích je někdy nutné dokázat vykreslit do terminálu (tedy neproporcionálním fontem) tabulky, v nichž je použita různá (a mnohdy dopředu neznámá) šířka sloupců. Tento problém má pochopitelně celou řadu řešení. Dnes se zaměříme na programovací jazyk Go, pro který vznikla trojice podobně pojmenovaných balíčků, které dokážou tabulky vykreslovat, a to různými styly: tabwriter, tablewriter a konečně tableprinter. Nejdříve si popíšeme možnosti balíčku nazvaného tabwriter, už jen z toho důvodu, že se jedná o standardní balíček programovacího jazyka Go (i když jeho možnosti jsou v porovnání s konkurencí omezené). tabwriter je založen na algoritmu elastic tabstops, který po analýze dat (zejména jejich šířky) dokáže tabulku korektně naformátovat. Tento algoritmus slouží i ke zobrazení zdrojových kódů – viz následující dnes již pravděpodobně dokonale známou animaci, která ukazuje základní funkcionalitu tohoto algoritmu, která je dostupná na adrese https://nickgravgaard.com/elastic-tabstops/images/columnblocks_coloured.gif:

2. Standardní balíček tabwriter
V úvodu dnešního článku se zaměříme na standardní balíček nazvaný tabwriter, resp. přesněji (s celou cestou) text/tabwriter. Tento balíček umožňuje zobrazit sloupcová data libovolného typu, přičemž jednotlivé sloupce jsou na vstupu ukončeny (nikoli odděleny) znakem Tab. Použití je ve skutečnosti velmi jednoduché, protože pouze postačuje vytvořit instanci této struktury a zapisovat do ní libovolnými I/O funkcemi, které akceptují rozhraní typu io.Writer, což jsou například mnohé funkce z dalšího standardního balíčku fmt. Konstruktoru se předává několik parametrů popsaných dále:
w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags)
Samotný zápis libovolných dat s definicí sloupců:
fmt.Fprintln(w, "1\t2\t3") fmt.Fprintln(w, "4\t5\t6") fmt.Fprintln(w, "7\t8\t9")
Nakonec je nutné zavolat metodu Flush, která zajistí vykreslení celé tabulky:
w.Flush()
Podívejme se na dnešní první demonstrační příklad, který po svém spuštění vypíše do terminálu dvojici tabulek, každou s odlišnou šířkou sloupců:
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 0 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = 0 ) func main() { w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t2\t3") fmt.Fprintln(w, "4\t5\t6") fmt.Fprintln(w, "7\t8\t9") w.Flush() fmt.Println() w = tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "foo\tbar\tbaz") fmt.Fprintln(w, "foo\tbar\tbaz") fmt.Fprintln(w, "foo\tbar\tbaz") w.Flush() }
Výsledek by měl vypadat následovně:
1 2 3 4 5 6 7 8 9 foo bar baz foo bar baz foo bar baz
3. Chování pro záznamy s odlišnou šířkou
Ve druhém demonstračním příkladu se opět pokusíme vykreslit dvě tabulky, tentokrát ovšem budou mít jednotlivé záznamy odlišnou šířku. Tomu se automaticky přizpůsobí výsledná tabulka (což je velký rozdíl oproti přímočarému použití znaků Tab, které tyto automatické úpravy nepodporují):
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 0 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = 0 ) func main() { w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() w = tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "f\tb\tb") fmt.Fprintln(w, "foo\tbar\tbaz") fmt.Fprintln(w, "foobar\tbarbaz\tbazfoo") w.Flush() }
Výsledek by měl vypadat takto:
1 1 1 22 22 22 333 333 333 4444 4444 4444 f b b foo bar baz foobar barbaz bazfoo
Šířku (přesněji řečeno minimální šířku) lze specifikovat i explicitně, a to konkrétně úpravou druhého parametru konstruktoru NewWriter. V dalším demonstračním příkladu minimální šířku postupně zvětšujeme od nuly až do devíti znaků:
package main import ( "fmt" "os" "text/tabwriter" ) const ( TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = 0 ) func main() { for minWidth := 0; minWidth < 10; minWidth++ { fmt.Printf("Min width = %d\n", minWidth) w := tabwriter.NewWriter(os.Stdout, minWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() } }
Na výsledku je patrné, že zpočátku nemá zadaná šířka žádný vliv na výslednou tabulku, protože záznamy jsou delší, než specifikovaná šířka. Posléze ovšem dojde k roztažení sloupců tabulky na požadovanou šířku:
Min width = 0 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 1 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 2 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 3 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 4 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 5 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 6 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 7 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 8 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 9 1 1 1 22 22 22 333 333 333 4444 4444 4444
4. Specifikace výplňových znaků včetně jejich minimálního počtu
Mezi další parametry konstruktoru NewWriter patří specifikace výplňového znaku (libovolný znak z celého Unicode, jak je ostatně v Go dobrým zvykem) a taktéž minimálního počtu výplňových znaků. Nejprve se podívejme, jak se změní formát výsledných tabulek, pokud budeme postupně měnit počet výplňových znaků:
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 0 TabWidth = 0 PaddingCharacter = ' ' Flags = 0 ) func main() { for padding := 0; padding < 10; padding++ { fmt.Printf("padding = %d\n", padding) w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() } }
Výsledné tabulky:
padding = 0 1 1 1 22 22 22 333 333 333 444444444444 padding = 1 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 2 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 3 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 4 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 5 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 6 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 7 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 8 1 1 1 22 22 22 333 333 333 4444 4444 4444 padding = 9 1 1 1 22 22 22 333 333 333 4444 4444 4444
Výplňovým znakem byla až doposud mezera, ovšem ve skutečnosti se může použít libovolný jiný znak, což je ukázáno na dalším demonstračním příkladu:
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 0 TabWidth = 0 PaddingCharacter = '.' Flags = 0 ) func main() { for padding := 0; padding < 10; padding++ { fmt.Printf("padding = %d\n", padding) w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() } }
Nyní je z výsledků patrné, jak se s výplňovými znaky pracuje v praxi:
padding = 0 1...1...1 22..22..22 333.333.333 444444444444 padding = 1 1....1....1 22...22...22 333..333..333 4444.4444.4444 padding = 2 1.....1.....1 22....22....22 333...333...333 4444..4444..4444 padding = 3 1......1......1 22.....22.....22 333....333....333 4444...4444...4444 padding = 4 1.......1.......1 22......22......22 333.....333.....333 4444....4444....4444 padding = 5 1........1........1 22.......22.......22 333......333......333 4444.....4444.....4444 padding = 6 1.........1.........1 22........22........22 333.......333.......333 4444......4444......4444 padding = 7 1..........1..........1 22.........22.........22 333........333........333 4444.......4444.......4444 padding = 8 1...........1...........1 22..........22..........22 333.........333.........333 4444........4444........4444 padding = 9 1............1............1 22...........22...........22 333..........333..........333 4444.........4444.........4444
5. Zarovnání hodnot doprava, příznak Debug
Posledním parametrem konstruktoru NewWriter je celočíselná hodnota s příznaky, které mění vlastnosti vykreslované tabulky. Jeden z těchto příznaků se jmenuje příznačně AlignRight. Pomocí tohoto příznaku lze hodnoty ve sloupcích zarovnat doprava a nikoli doleva (což je výchozí chování):
package main import ( "fmt" "os" "text/tabwriter" ) const ( TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight ) func main() { for minWidth := 0; minWidth < 10; minWidth++ { fmt.Printf("Min width = %d\n", minWidth) w := tabwriter.NewWriter(os.Stdout, minWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() } }
Výsledkem jsou tabulky se třemi sloupci, které ovšem vypadají „divně“. Proč tomu tak je, si vysvětlíme v dalším textu:
Min width = 0 1 11 22 2222 333 333333 4444 44444444 Min width = 1 1 11 22 2222 333 333333 4444 44444444 Min width = 2 1 11 22 2222 333 333333 4444 44444444 Min width = 3 1 11 22 2222 333 333333 4444 44444444 Min width = 4 1 11 22 2222 333 333333 4444 44444444 Min width = 5 1 11 22 2222 333 333333 4444 44444444 Min width = 6 1 11 22 2222 333 333333 4444 44444444 Min width = 7 1 11 22 2222 333 333333 4444 44444444 Min width = 8 1 11 22 2222 333 333333 4444 44444444 Min width = 9 1 11 22 2222 333 333333 4444 44444444
Druhým užitečným příznakem je příznak nazvaný Debug. Tímto příznakem se povoluje zobrazení oddělovačů mezi sloupci. Oddělovačem je ve výchozím nastavení znak „|“:
package main import ( "fmt" "os" "text/tabwriter" ) const ( TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) func main() { for minWidth := 0; minWidth < 10; minWidth++ { fmt.Printf("Min width = %d\n", minWidth) w := tabwriter.NewWriter(os.Stdout, minWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1") fmt.Fprintln(w, "22\t22\t22") fmt.Fprintln(w, "333\t333\t333") fmt.Fprintln(w, "4444\t4444\t4444") w.Flush() fmt.Println() } }
Nyní jsou – alespoň teoreticky – sloupce zarovnané doprava a současně jsou od sebe oddělené znakem „|“:
Min width = 0 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 1 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 2 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 3 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 4 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 5 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 6 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 7 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 8 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444 Min width = 9 1| 1|1 22| 22|22 333| 333|333 4444| 4444|4444
6. Skutečný význam znaku Tab
Předchozí dvojice demonstračních příkladů nepracovala korektně, protože poslední (třetí) sloupec ve skutečnosti nebyl zarovnán doprava, ale doleva. Je tomu tak z toho důvodu, že znak Tab slouží pro ukončení sloupců, nikoli pro jejich oddělení. V praxi to znamená to, že je nutné uvést znak Tab i na konci vypisovaného řetězce (přesněji řečeno před znakem pro konec řádku):
package main import ( "fmt" "os" "text/tabwriter" ) const ( TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight ) func main() { for minWidth := 0; minWidth < 10; minWidth++ { fmt.Printf("Min width = %d\n", minWidth) w := tabwriter.NewWriter(os.Stdout, minWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1\t") fmt.Fprintln(w, "22\t22\t22\t") fmt.Fprintln(w, "333\t333\t333\t") fmt.Fprintln(w, "4444\t4444\t4444\t") w.Flush() fmt.Println() } }
Tento demonstrační příklad již zobrazí všechny sloupce s korektním zarovnáním:
Min width = 0 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 1 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 2 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 3 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 4 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 5 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 6 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 7 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 8 1 1 1 22 22 22 333 333 333 4444 4444 4444 Min width = 9 1 1 1 22 22 22 333 333 333 4444 4444 4444
Totéž bude platit i při využití příznaku Debug, kterým zajistíme oddělení hodnot ve sloupcích znakem „|“:
package main import ( "fmt" "os" "text/tabwriter" ) const ( TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) func main() { for minWidth := 0; minWidth < 10; minWidth++ { fmt.Printf("Min width = %d\n", minWidth) w := tabwriter.NewWriter(os.Stdout, minWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "1\t1\t1\t") fmt.Fprintln(w, "22\t22\t22\t") fmt.Fprintln(w, "333\t333\t333\t") fmt.Fprintln(w, "4444\t4444\t4444\t") w.Flush() fmt.Println() } }
S výsledkem:
Min width = 0 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 1 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 2 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 3 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 4 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 5 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 6 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 7 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 8 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444| Min width = 9 1| 1| 1| 22| 22| 22| 333| 333| 333| 4444| 4444| 4444|
7. Zobrazení tabulky s faktoriály hodnot 0 až 20
V dalším demonstračním příkladu, který bude ještě několikrát zopakován, ovšem pokaždé při použití odlišného balíčku, je ukázán způsob zobrazení tabulky s faktoriály hodnot 0 až 20. Výsledky velmi rychle rostou, ovšem tabulka se bez problémů přizpůsobí různé šířce hodnot:
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 5 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func main() { w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "n\tn!\t") for n := 0; n <= 20; n++ { fmt.Fprintf(w, "%d\t%d\t\n", n, Factorial(int64(n))) } w.Flush() }
Výsledná tabulka zobrazená na terminálu by měla vypadat následovně:
n| n!| 0| 1| 1| 1| 2| 2| 3| 6| 4| 24| 5| 120| 6| 720| 7| 5040| 8| 40320| 9| 362880| 10| 3628800| 11| 39916800| 12| 479001600| 13| 6227020800| 14| 87178291200| 15| 1307674368000| 16| 20922789888000| 17| 355687428096000| 18| 6402373705728000| 19| 121645100408832000| 20| 2432902008176640000|
8. Postupné přidávání hodnot do výsledné tabulky funkcí fmt.Fprintf
Při konstrukci výsledné tabulky se balíček text/tabwriter ovládá dvěma řídicími znaky – Tab a Newline, tedy v programovém zápisu ‚\t‘ a ‚\n‘. Znak nového řádku vede k vykreslení jednoho řádku tabulky, znak Tab pak k ukončení položky záznamu. Co to ovšem znamená v praxi? Není nutné celý řádek tabulky vypsat jedinou funkcí fmt.Println či nějakou podobnou funkcí, která na výstup posílá i znak pro konec řádku. Naopak – jednotlivé položky záznamu je možné přidávat postupně, což může být i přehlednější. Viz též následující demonstrační příklad:
package main import ( "fmt" "os" "text/tabwriter" ) const ( MinWidth = 5 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func main() { w := tabwriter.NewWriter(os.Stdout, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "n\tn!\t") for n := 0; n <= 20; n++ { fmt.Fprintf(w, "%d\t", n) result := Factorial(int64(n)) fmt.Fprintf(w, "%d\t", result) fmt.Fprintln(w) } w.Flush() }
Výsledek by měl být totožný s příkladem, který byl ukázán v předchozí kapitole:
n| n!| 0| 1| 1| 1| 2| 2| 3| 6| 4| 24| 5| 120| 6| 720| 7| 5040| 8| 40320| 9| 362880| 10| 3628800| 11| 39916800| 12| 479001600| 13| 6227020800| 14| 87178291200| 15| 1307674368000| 16| 20922789888000| 17| 355687428096000| 18| 6402373705728000| 19| 121645100408832000| 20| 2432902008176640000|
9. Zápis tabulky do souboru
Tabulku je možné ve skutečnosti poslat (neboli zobrazit) s využitím libovolné struktury, která implementuje rozhraní Writer. Zcela triviálně je tak podporován zápis do souboru, což je ostatně ukázáno v dnešním dvanáctém demonstračním příkladu:
package main import ( "fmt" "log" "os" "text/tabwriter" ) const ( MinWidth = 5 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func main() { file, err := os.Create("table1.txt") if err != nil { log.Fatal(err) } defer file.Close() w := tabwriter.NewWriter(file, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "n\tn!\t") for n := 0; n <= 20; n++ { fmt.Fprintf(w, "%d\t", n) result := Factorial(int64(n)) fmt.Fprintf(w, "%d\t", result) fmt.Fprintln(w) } w.Flush() }
Po spuštění tohoto příkladu se vytvoří soubor „table1.txt“, jehož obsah je následující:
n| n!| 0| 1| 1| 1| 2| 2| 3| 6| 4| 24| 5| 120| 6| 720| 7| 5040| 8| 40320| 9| 362880| 10| 3628800| 11| 39916800| 12| 479001600| 13| 6227020800| 14| 87178291200| 15| 1307674368000| 16| 20922789888000| 17| 355687428096000| 18| 6402373705728000| 19| 121645100408832000| 20| 2432902008176640000|
10. HTTP server, který nabízí klientům naformátovanou tabulku
Poslední demonstrační příklad založený na balíčku text/tabwriter se od předchozích příkladů odlišuje. Jedná se totiž o implementaci HTTP serveru, jehož jediný endpoint slouží pro získání naformátované tabulky. Základní funkcionalita tohoto příkladu je založena na tom, že obsluha (handler) zajišťující přístup k endpointu pracuje s datovou strukturou implementující rozhraní io.Writer (přesněji řečeno s datovou strukturou, která danému rozhraní vyhovuje, abychom se drželi ustálené terminologie):
package main import ( "fmt" "net/http" "text/tabwriter" ) const ( MinWidth = 5 TabWidth = 0 Padding = 1 PaddingCharacter = ' ' Flags = tabwriter.AlignRight | tabwriter.Debug ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { w := tabwriter.NewWriter(writer, MinWidth, TabWidth, Padding, PaddingCharacter, Flags) fmt.Fprintln(w, "n\tn!\t") for n := 0; n <= 20; n++ { fmt.Fprintf(w, "%d\t", n) result := Factorial(int64(n)) fmt.Fprintf(w, "%d\t", result) fmt.Fprintln(w) } w.Flush() } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
Po spuštění tohoto příkladu je možné v jiném terminálu přistoupit na adresu localhost:8000, například nástrojem curl:
$ curl localhost:8000/ n| n!| 0| 1| 1| 1| 2| 2| 3| 6| 4| 24| 5| 120| 6| 720| 7| 5040| 8| 40320| 9| 362880| 10| 3628800| 11| 39916800| 12| 479001600| 13| 6227020800| 14| 87178291200| 15| 1307674368000| 16| 20922789888000| 17| 355687428096000| 18| 6402373705728000| 19| 121645100408832000| 20| 2432902008176640000|
11. Balíček olekukonko/tablewriter
Druhý balíček určený pro práci s tabulkami v programovacím jazyku Go se jmenuje olekukonko/tablewriter. Jedná se o nestandardní balíček, který je nutné explicitně nainstalovat:
$ go get github.com/olekukonko/tablewriter
Od této chvíle je možné balíček používat v jakémkoli projektu naprogramovaného v jazyce Go.
S tímto balíčkem se pracuje poněkud odlišným způsobem, než se standardním balíčkem text/tabwriter. Hlavní rozdíl spočívá v konstrukci tabulek – nepoužívají se zde řetězce se znaky Tab, ale tabulka je konstruována z řezů řetězců (string slice), což vyžaduje poněkud odlišný přístup ke struktuře kódu, který tabulky generuje. Možnosti balíčku tablewriter jsou větší, než u standardního balíčku tabwriter, protože je podporován například výstup s využitím barev, orámováním tabulky, tabulka může obsahovat hlavičku a patičku atd. Některé možnosti jsou ukázány v navazujících kapitolách.
12. Vykreslení tabulky s hlavičkou i s okraji buněk
V následujícím kódu, který je mimochodem převzat prakticky beze změny přímo ze stránky balíčku tablewriter, je ukázána konstrukce tabulky. Nejprve je nutné zkonstruovat objekt reprezentující tabulku; k tomuto účelu slouží konstruktor NewWriter, jemuž se opět předává libovolná struktura vyhovující rozhraní io.Writer. Následně je možné specifikovat hlavičku tabulky metodou SetHeader, přidat do tabulky jednotlivé řádky metodou Append a na konci tabulku vykreslit metodou Render:
package main import ( "os" "github.com/olekukonko/tablewriter" ) func main() { data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) for _, v := range data { table.Append(v) } table.Render() }
Výsledná tabulka vypadá značně odlišně od předchozích tabulek:
+------+-----------------------+--------+ | NAME | SIGN | RATING | +------+-----------------------+--------+ | A | The Good | 500 | | B | The Very very Bad Man | 288 | | C | The Ugly | 120 | | D | The Gopher | 800 | +------+-----------------------+--------+
13. Tabulka bez okrajů
Po konstrukci datové struktury představující tabulku konstruktorem NewWriter je možné modifikovat některé vlastnosti tabulky. Týká se to například zobrazení tabulky bez okrajů – pokud okraje z nějakého důvodu nepotřebujete zobrazit, postačuje zavolat metodu SetBorder a předat jí pravdivostní hodnotu false:
package main import ( "os" "github.com/olekukonko/tablewriter" ) func main() { data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) table.SetBorder(false) for _, v := range data { table.Append(v) } table.Render() }
Výsledná tabulka by měla vypadat následovně:
NAME | SIGN | RATING -------+-----------------------+--------- A | The Good | 500 B | The Very very Bad Man | 288 C | The Ugly | 120 D | The Gopher | 800
14. Tabulka s patičkou (suma hodnot atd.)
Poměrně užitečná je možnost zobrazit tabulku nejenom s hlavičkou, ale i s patičkou. K přidání patičky slouží metoda SetFooter, která akceptuje stejné parametry, jako metody SetHeader i Append:
package main import ( "os" "github.com/olekukonko/tablewriter" ) func main() { data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) table.SetFooter([]string{"", "Sum", "1708"}) for _, v := range data { table.Append(v) } table.Render() }
Tabulka bude zobrazena následujícím způsobem (povšimněte si buňky s chybějící hodnotou v patičce):
+------+-----------------------+--------+ | NAME | SIGN | RATING | +------+-----------------------+--------+ | A | The Good | 500 | | B | The Very very Bad Man | 288 | | C | The Ugly | 120 | | D | The Gopher | 800 | +------+-----------------------+--------+ | SUM | 1708 | +------+-----------------------+--------+
Prakticky stejný příklad, ovšem s tím nepatrným rozdílem, že tabulka nebude obsahovat okraje:
package main import ( "os" "github.com/olekukonko/tablewriter" ) func main() { data := [][]string{ []string{"A", "The Good", "500"}, []string{"B", "The Very very Bad Man", "288"}, []string{"C", "The Ugly", "120"}, []string{"D", "The Gopher", "800"}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Name", "Sign", "Rating"}) table.SetFooter([]string{"", "Sum", "1708"}) table.SetBorder(false) for _, v := range data { table.Append(v) } table.Render() }
Výsledná tabulka vygenerovaná předchozím příkladem:
NAME | SIGN | RATING -------+-----------------------+--------- A | The Good | 500 B | The Very very Bad Man | 288 C | The Ugly | 120 D | The Gopher | 800 -------+-----------------------+--------- SUM | 1708 ------------------------+---------
15. Zobrazení tabulky s faktoriály hodnot 0 až 20
Opět se pokusme zobrazit tabulku s faktoriály hodnot 0 až 20, tentokrát ovšem s využitím balíčku tablewriter a nikoli tabwriter. Programový kód bude v tomto případě složitější, protože je nutné převádět celočíselné hodnoty na řetězce. Je tomu tak z toho důvodu, že metoda Append akceptuje pole řetězců – viz též dokumentaci:
package main import ( "os" "strconv" "github.com/olekukonko/tablewriter" ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func main() { table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"n", "n!"}) for n := 0; n <= 20; n++ { f := Factorial(int64(n)) row := []string{strconv.Itoa(n), strconv.FormatInt(f, 10)} table.Append(row) } table.Render() }
Výsledná tabulka:
+----+---------------------+ | N | N! | +----+---------------------+ | 0 | 1 | | 1 | 1 | | 2 | 2 | | 3 | 6 | | 4 | 24 | | 5 | 120 | | 6 | 720 | | 7 | 5040 | | 8 | 40320 | | 9 | 362880 | | 10 | 3628800 | | 11 | 39916800 | | 12 | 479001600 | | 13 | 6227020800 | | 14 | 87178291200 | | 15 | 1307674368000 | | 16 | 20922789888000 | | 17 | 355687428096000 | | 18 | 6402373705728000 | | 19 | 121645100408832000 | | 20 | 2432902008176640000 | +----+---------------------+
16. Výstup do souboru, HTTP server nabízející klientům naformátovanou tabulku
Protože balíček tablewriter dokáže pracovat s každou datovou strukturou vyhovující rozhraní io.Writer, je podporováno i vykreslení tabulky do textového souboru, což je ukázáno v dalším demonstračním příkladu. Ten po svém spuštění vytvoří soubor „table2.txt“ a zapíše do něj příslušnou naformátovanou tabulku s hodnotami faktoriálů:
package main import ( "log" "os" "strconv" "github.com/olekukonko/tablewriter" ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func main() { file, err := os.Create("table2.txt") if err != nil { log.Fatal(err) } defer file.Close() table := tablewriter.NewWriter(file) table.SetHeader([]string{"n", "n!"}) for n := 0; n <= 20; n++ { f := Factorial(int64(n)) row := []string{strconv.Itoa(n), strconv.FormatInt(f, 10)} table.Append(row) } table.Render() }
+----+---------------------+ | N | N! | +----+---------------------+ | 0 | 1 | | 1 | 1 | | 2 | 2 | | 3 | 6 | | 4 | 24 | | 5 | 120 | | 6 | 720 | | 7 | 5040 | | 8 | 40320 | | 9 | 362880 | | 10 | 3628800 | | 11 | 39916800 | | 12 | 479001600 | | 13 | 6227020800 | | 14 | 87178291200 | | 15 | 1307674368000 | | 16 | 20922789888000 | | 17 | 355687428096000 | | 18 | 6402373705728000 | | 19 | 121645100408832000 | | 20 | 2432902008176640000 | +----+---------------------+
Vzhledem k tomu, že balíček tablewriter dokáže tabulku vypsat do libovolné struktury vyhovující rozhraní io.Writer, je možné (opět) relativně snadno implementovat HTTP server, jehož obslužný kód endpointu takovou tabulku vytvoří a pošle zpět klientovi. Jedná se tedy o kombinaci příkladu z předchozí kapitoly z příkladem z kapitoly desáté:
package main import ( "net/http" "strconv" "github.com/olekukonko/tablewriter" ) // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { table := tablewriter.NewWriter(writer) table.SetHeader([]string{"n", "n!"}) for n := 0; n <= 20; n++ { f := Factorial(int64(n)) row := []string{strconv.Itoa(n), strconv.FormatInt(f, 10)} table.Append(row) } table.Render() } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
Po spuštění HTTP serveru přistoupíme k (jedinému) koncovému bodu a necháme si serverem poslat naformátovanou tabulku:
$ curl localhost:8000/ +----+---------------------+ | N | N! | +----+---------------------+ | 0 | 1 | | 1 | 1 | | 2 | 2 | | 3 | 6 | | 4 | 24 | | 5 | 120 | | 6 | 720 | | 7 | 5040 | | 8 | 40320 | | 9 | 362880 | | 10 | 3628800 | | 11 | 39916800 | | 12 | 479001600 | | 13 | 6227020800 | | 14 | 87178291200 | | 15 | 1307674368000 | | 16 | 20922789888000 | | 17 | 355687428096000 | | 18 | 6402373705728000 | | 19 | 121645100408832000 | | 20 | 2432902008176640000 | +----+---------------------+
17. Balíček lensesio/tableprinter
Poslední, v pořadí již třetí balíček, o němž se v dnešním článku zmíníme, se jmenuje tableprinter, resp. celým jménem lensesio/tableprinter. Tento balíček je založen na balíčku tablewriter, ovšem rozšiřuje jeho možnosti, a to hned několika způsoby. Dnes se seznámíme jen se základními možnostmi nabízenými tímto balíčkem; případné podrobnosti (barevný výstup atd.) budou vysvětleny v navazujícím článku.
Opět se jedná o nestandardní balíček, takže je nutná jeho explicitní instalace:
$ go get -u github.com/lensesio/tableprinter
Od této chvíle je možné balíček používat v jakémkoli projektu naprogramovaného v jazyce Go.
Struktura dat, která se mají vykreslit v tabulce, je definována novým datovým typem, v němž lze u jednotlivých položek specifikovat i názvy sloupců (podobně se ostatně pracuje se soubory TOML, JSON atd.):
type Record struct { Rank string `header:"rank"` Title string `header:"title"` Value int `header:"value"` }
Samotné vytvoření a vykreslení tabulky je jednoduché – nejprve se vytvoří instance objektu představujícího tabulku a poté se metodou Print tomuto objektu předají data, která se mají vykreslit:
table := tableprinter.New(os.Stdout) table.Print(data)
18. Ukázky použití balíčku lensesio/tableprinter
V této kapitole si – prozatím ovšem pouze ve stručnosti – ukážeme některé možnosti, které nám nabízí balíček tableprinter při tisku tabulek. První demonstrační příklad je nejjednodušší, protože vykreslí tabulku výchozím stylem bez dalších úprav:
package main import ( "os" "github.com/lensesio/tableprinter" ) type Record struct { Rank string `header:"rank"` Title string `header:"title"` Value int `header:"value"` } func main() { data := []Record{ {"A", "The Good", 500}, {"B", "The Very very Bad Man", 288}, {"C", "The Ugly", 120}, {"D", "The Gopher", 800}, } table := tableprinter.New(os.Stdout) table.Print(data) }
Výsledkem bude tato tabulka:
RANK (4) TITLE VALUE ----------- ----------------------- ------- A The Good 500 B The Very very Bad Man 288 C The Ugly 120 D The Gopher 800
Můžeme ovšem taktéž explicitně specifikovat styl okrajů tabulky:
package main import ( "os" "github.com/lensesio/tableprinter" ) type Record struct { Rank string `header:"rank"` Title string `header:"title"` Value int `header:"value"` } func main() { data := []Record{ {"A", "The Good", 500}, {"B", "The Very very Bad Man", 288}, {"C", "The Ugly", 120}, {"D", "The Gopher", 800}, } table := tableprinter.New(os.Stdout) table.BorderTop, table.BorderBottom, table.BorderLeft, table.BorderRight = true, true, true, true table.Print(data) }
S výsledkem:
----------- ----------------------- ------- RANK (4) TITLE VALUE ----------- ----------------------- ------- A The Good 500 B The Very very Bad Man 288 C The Ugly 120 D The Gopher 800 ----------- ----------------------- -------
Určit je možné i znaky použité pro tisk okrajů. Podporováno je celé Unicode, samozřejmě pouze v případě použití fontu se všemi potřebnými znaky:
package main import ( "os" "github.com/lensesio/tableprinter" ) type Record struct { Rank string `header:"rank"` Title string `header:"title"` Value int `header:"value"` } func main() { data := []Record{ {"A", "The Good", 500}, {"B", "The Very very Bad Man", 288}, {"C", "The Ugly", 120}, {"D", "The Gopher", 800}, } table := tableprinter.New(os.Stdout) table.BorderTop, table.BorderBottom, table.BorderLeft, table.BorderRight = true, true, true, true table.CenterSeparator = "│" table.ColumnSeparator = "│" table.RowSeparator = "─" table.Print(data) }
S výsledkem:
│───────────│───────────────────────│───────│ │ RANK (4) │ TITLE │ VALUE │ │───────────│───────────────────────│───────│ │ A │ The Good │ 500 │ │ B │ The Very very Bad Man │ 288 │ │ C │ The Ugly │ 120 │ │ D │ The Gopher │ 800 │ │───────────│───────────────────────│───────│
Tisk tabulky s faktoriály:
package main import ( "os" "github.com/lensesio/tableprinter" ) type Record struct { N int `header:"n"` F int64 `header:"n!"` } // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } const MaxN = 17 func main() { data := make([]Record, 0) for n := 0; n <= MaxN; n++ { data = append(data, Record{n, Factorial(int64(n))}) } table := tableprinter.New(os.Stdout) table.BorderTop, table.BorderBottom, table.BorderLeft, table.BorderRight = true, true, true, true table.CenterSeparator = "│" table.ColumnSeparator = "│" table.RowSeparator = "─" table.Print(data) }
Povšimněte si, jak se velké hodnoty implicitně zkracují (což nemusí být vždy výhodné):
│─────────│────────│ │ N (18) │ N! │ │─────────│────────│ │ 0 │ 1 │ │ 1 │ 1 │ │ 2 │ 2 │ │ 3 │ 6 │ │ 4 │ 24 │ │ 5 │ 120 │ │ 6 │ 720 │ │ 7 │ 5.4K │ │ 8 │ 40.3K │ │ 9 │ 362.8K │ │ 10 │ 3.6M │ │ 11 │ 39.9M │ │ 12 │ 479.1M │ │ 13 │ 6.2B │ │ 14 │ 87.1B │ │ 15 │ 1.3T │ │ 16 │ 20.9T │ │ 17 │ 355.6T │ │─────────│────────│
Výstup do souboru:
package main import ( "log" "os" "github.com/lensesio/tableprinter" ) type Record struct { N int `header:"n"` F int64 `header:"n!"` } // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } const MaxN = 17 func main() { file, err := os.Create("table3.txt") if err != nil { log.Fatal(err) } defer file.Close() data := make([]Record, 0) for n := 0; n <= MaxN; n++ { data = append(data, Record{n, Factorial(int64(n))}) } table := tableprinter.New(file) table.BorderTop, table.BorderBottom, table.BorderLeft, table.BorderRight = true, true, true, true table.CenterSeparator = "│" table.ColumnSeparator = "│" table.RowSeparator = "─" table.Print(data) }
Výsledkem bude soubor „table3.txt“ s tímto obsahem:
│─────────│────────│ │ N (18) │ N! │ │─────────│────────│ │ 0 │ 1 │ │ 1 │ 1 │ │ 2 │ 2 │ │ 3 │ 6 │ │ 4 │ 24 │ │ 5 │ 120 │ │ 6 │ 720 │ │ 7 │ 5.4K │ │ 8 │ 40.3K │ │ 9 │ 362.8K │ │ 10 │ 3.6M │ │ 11 │ 39.9M │ │ 12 │ 479.1M │ │ 13 │ 6.2B │ │ 14 │ 87.1B │ │ 15 │ 1.3T │ │ 16 │ 20.9T │ │ 17 │ 355.6T │ │─────────│────────│
A konečně obdoba HTTP serveru z předchozích kapitol:
package main import ( "net/http" "github.com/lensesio/tableprinter" ) type Record struct { N int `header:"n"` F int64 `header:"n!"` } // Factorial computes factorial for given n that might be positive integer func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } const MaxN = 17 func mainEndpoint(writer http.ResponseWriter, request *http.Request) { data := make([]Record, 0) for n := 0; n <= MaxN; n++ { data = append(data, Record{n, Factorial(int64(n))}) } table := tableprinter.New(writer) table.BorderTop, table.BorderBottom, table.BorderLeft, table.BorderRight = true, true, true, true table.CenterSeparator = "│" table.ColumnSeparator = "│" table.RowSeparator = "─" table.Print(data) } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Standardní balíček text/tabwriter
https://golang.org/pkg/text/tabwriter/ - Elastic tabstops: A better way to indent and align code
https://nickgravgaard.com/elastic-tabstops/ - ASCII Table Writer
https://github.com/olekukonko/tablewriter - TablePrinter
https://github.com/lensesio/tableprinter - go-pretty
https://github.com/jedib0t/go-pretty - What are the drawbacks of elastic tabstops?
https://softwareengineering.stackexchange.com/questions/137290/what-are-the-drawbacks-of-elastic-tabstops - Elastic tabstop editors and plugins
https://stackoverflow.com/questions/28652/elastic-tabstop-editors-and-plugins - Příkaz gofmt
https://golang.org/cmd/gofmt/ - Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/