聊聊 Go 如何打包與壓縮檔案

語言: CN / TW / HK

檔案的打包、壓縮與解壓縮是經常會使用到的功能,我們可以通過 tar、gzip 等工具來完成這些操作。在 Go 中,標準庫archive與compress為我們提供了這些能力,通過本文示例,你會發現以 Go 程式設計的方式生成與處理壓縮打包檔案也非常簡單。

打包和壓縮

在開始程式碼之前,我們需要明確打包和壓縮的概念。

  • 打包,又被稱為歸檔,指的是一個檔案或目錄的集合,而這個集合被儲存在一個檔案中。
  • 壓縮,指的是利用演算法將檔案進行處理,以達到保留最大檔案資訊,而讓檔案體積變小的目的。

以打包工具 tar 為例,通過其打出來的檔案通常稱為 tar 包,其檔案命名通常以 .tar 結尾。再通過其他的壓縮工具對 tar 包進行壓縮,例如 gzip 壓縮,則得到通常以 .tar.gz 結尾命名的壓縮檔案(在 tar 中可使用 -z 引數來呼叫gzip)。

tar 包是檔案的集合,其結構也是由資料段組成的,每塊資料段包含了檔案頭(描述檔案的元資訊)和檔案內容。

+----------------------------------------+
| Header                                 |
| [name][mode][owner][group][size]  ...  |
+----------------------------------------+
| Content                                |
| XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
| XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
+----------------------------------------+
| Header                                 |
| [name][mode][owner][group][size]  ...  |
+----------------------------------------+
| Content                                |
| XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
| XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX|
+----------------------------------------+
| ...                                     |

archive 庫打包與解包​

archive 庫的中文含義是檔案,它的作用就是歸檔(打包)與拆檔(解包)。其提供兩種方案:tar 與 zip,呼叫路徑分別為archive/tar和archive/zip。

我們以 tar 為例,來展示如何實現檔案的打包與解包。

首先,新建目標打包檔案為 out.tar,再構造一些檔案資料 readme.txt、gopher.txt 和 todo.txt 用於歸檔。

import (
 "archive/tar"
  ...
)
func main() {
 // Create and add some files to the archive.
 tarPath := "out.tar"
 tarFile, err := os.Create(tarPath)
 if err != nil {
  log.Fatal(err)
 }
 defer tarFile.Close()
 tw := tar.NewWriter(tarFile)
 defer tw.Close()
 var files = []struct {
  Name, Body string
 }{
  {"readme.txt", "This archive contains some text files."},
  {"gopher.txt", "Gopher names:\nGeorge\nGeoffrey\nGonzo"},
  {"todo.txt", "Get animal handling license."},
 }
 ... 
}

接著依次構建檔案頭資訊,分別指定了檔名、許可權和大小(可定義更多檔案頭欄位),再通過*tar.Writer​型別的 tw 變數,按序呼叫WriteHeader和Write方法將需要打包的資料段(檔案頭+檔案內容)寫入到out.tar檔案。

...
 for _, file := range files {
  hdr := &tar.Header{
   Name: file.Name,
   Mode: 0600,
   Size: int64(len(file.Body)),
  }
  if err := tw.WriteHeader(hdr); err != nil {
   log.Fatal(err)
  }
  if _, err := tw.Write([]byte(file.Body)); err != nil {
   log.Fatal(err)
  }
 }
}

執行以上程式碼,將得到打包後的 out.tar 檔案,可通過 tar 工具指定 -tvf 引數檢視歸檔資訊。

$ tar -tvf out.tar
-rw-------  0 0      0          38 Jan  1  1970 readme.txt
-rw-------  0 0      0          35 Jan  1  1970 gopher.txt
-rw-------  0 0      0          28 Jan  1  1970 todo.txt

可以看到,指定的檔案資訊(檔名、許可權和大小)符合預期,但其他未指定的元資訊是有誤的,例如日期(直接給的預設值)。

如果通過 tar 工具,我們可以執行以下命令來提取 out.tar 中的檔案。

$ tar -xvf out.tar
x readme.txt
x gopher.txt
x todo.txt

但在程式中實現,應該怎麼做呢?

func main() {
 tarPath := "out.tar"
 tarFile, err := os.Open(tarPath)
 if err != nil {
  log.Fatal(err)
 }
 defer tarFile.Close()
 tr := tar.NewReader(tarFile)
 for {
  hdr, err := tr.Next()
  // End of archive
  if err == io.EOF {
   break
  }
  if err != nil {
   log.Fatal(err)
  }
  fmt.Printf("Contents of %s: ", hdr.Name)
  if _, err := io.Copy(os.Stdout, tr); err != nil {
   log.Fatal(err)
  }
  fmt.Println()
 }
}

// Output:
Contents of readme.txt: This archive contains some text files.
Contents of gopher.txt: Gopher names:
George
Geoffrey
Gonzo
Contents of todo.txt: Get animal handling license.

首先需要開啟 out.tar,並構造*tar.Reader​型別的 tr 變數。之後,利用tr.Next​依次提取每個資料段內容,並通過 io.Copy(os.Stdout, tr),將檔案內容拷貝至標準輸出。直到tr.Next​遇到io.EOF,它代表讀取到了歸檔檔案末尾,則退出提取。

compress 庫壓縮與解壓縮

compress 庫中支援了多種壓縮方案,包括 bzip2、flate、gzip、lzw 和 zlib,呼叫路徑為compress/xxx。

我們以常用的 gzip 為例,來展示壓縮與解壓縮程式碼。

如果同樣是上文中的檔案資料 readme.txt、gopher.txt 和 todo.txt,我們想得到 tar 歸檔且被壓縮了的 out.tar.gz 檔案,應該如何做呢?

package main

import (
 "archive/tar"
 "compress/gzip"
 ...
)

func main() {
 tarPath := "out.tar.gz"
 tarFile, err := os.Create(tarPath)
 if err != nil {
  log.Fatal(err)
 }
 defer tarFile.Close()
 gz := gzip.NewWriter(tarFile)
 defer gz.Close()
 tw := tar.NewWriter(gz)
 defer tw.Close()
 ...
}

非常簡單!只需要將tar.NewWriter(tarFile)​改為tar.NewWriter(gz)​即可,其中gz​是由gzip.NewWriter(tarFile) 而來。

我們比較有壓縮與無壓縮的歸檔 tar 包大小,可以看到檔案體積從4.0K壓縮為了224B。

$ ls -alh out.tar out.tar.gz
-rw-r--r--  1 slp  staff   4.0K Jul  3 21:52 out.tar
-rw-r--r--  1 slp  staff   224B Jul  3 21:53 out.tar.gz

同理,如果要解壓並解包 out.tar.gz 檔案,應該如何做呢?

package main

import (
 "archive/tar"
 "compress/gzip"
  ...
)

func main() {
 tarPath := "out.tar.gz"
 tarFile, err := os.Open(tarPath)
 if err != nil {
  log.Fatal(err)
 }
 defer tarFile.Close()
 gz, err := gzip.NewReader(tarFile)
 if err != nil {
  log.Fatal(err)
 }
  defer gz.Close()
 tr := tar.NewReader(gz)
  ...
}

依然很簡單!只需要將tar.NewReader(tarFile)​改為tar.NewReader(gz)​即可,其中gz​是由gzip.NewReader(tarFile) 而來。

總結

本文展示瞭如何通過archive/tar​包實現檔案的打包與解包操作,如何通過compress/gzip包對tar包開展進一步的壓縮與解壓縮。

在展示compress/gzip使用時,多封裝一層Writer/Reader,即可為tar歸檔檔案增加壓縮與解壓縮功能。更棒的是,如果你想切換打包/解包、壓縮/解壓縮策略,僅僅替換掉對應的 Writer/Reader 即可。而這種便利,源於 Go 優秀的流式 IO 設計。

當然,紙上得來終覺淺,絕知此事要躬行。沒有使用過archive和compress庫的讀者,可以嘗試用本文未使用過的方案,來試著處理打包壓縮檔案。​