整合營銷服務商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          GCTT 出品 - 使用 Go 語言完成 HTTP

          GCTT 出品 - 使用 Go 語言完成 HTTP 文件上傳與下載

          近我使用 Go 語言完成了一個正式的 web 應用,有一些方面的問題在使用 Go 開發(fā) web 應用過程中比較重要。過去,我將 web 開發(fā)作為一項職業(yè)并且把使用不同的語言和范式開發(fā) web 應用作為一項愛好,因此對于 web 開發(fā)領域有一些心得體會。

          總的來說,我喜歡使用 Go 語言進行 web 開發(fā),盡管開始一段時間需要去適應它。Go 語言有一些坑,但是正如本篇文章中所要討論的文件上傳與下載,Go 語言的標準庫與內置函數(shù),使得開發(fā)是種愉快的體驗。

          在接下來的幾篇文章中,我將重點討論我在 Go 中編寫生產級 Web 應用程序時遇到的一些問題,特別是關于身份驗證/授權的問題。

          這篇文章將展示HTTP文件上傳和下載的基本示例。我們將一個有 type 文本框和一個 uploadFile 上傳框的 HTML 表單作為客戶端。

          讓我們來看下 Go 語言中是如何解決這種在 web 開發(fā)中隨處可見的問題的。

          代碼示例

          首先,我們在服務器端設定兩個路由,/upload 用于文件上傳, /files/* 用于文件下載。

          const maxUploadSize=2 * 1024 * 2014 // 2 MB
          const uploadPath="./tmp"
          func main() {
           http.HandleFunc("/upload", uploadFileHandler())
           fs :=http.FileServer(http.Dir(uploadPath))
           http.Handle("/files/", http.StripPrefix("/files", fs))
           log.Print("Server started on localhost:8080, use /upload for uploading files and /files/{fileName} for downloading files.")
           log.Fatal(http.ListenAndServe(":8080", nil))
          }
          

          我們還將要上傳的目標目錄,以及我們接受的最大文件大小定義為常量。注意這里,整個文件服務的概念是如此的簡單 —— 我們僅使用標準庫中的工具,使用 http.FileServe 創(chuàng)建一個 HTTP 處理程序,它將使用 http.Dir(uploadPath) 提供的目錄來上傳文件。

          現(xiàn)在我們只需要實現(xiàn) uploadFileHandler。

          這個處理程序將包含以下功能:

          • 驗證文件最大值
          • 從請求驗證文件和 POST 參數(shù)
          • 檢查所提供的文件類型(我們只接受圖像和 PDF)
          • 創(chuàng)建一個隨機文件名
          • 將文件寫入硬盤
          • 處理所有錯誤,如果一切順利返回成功消息

          第一步,我們定義處理程序:

          func uploadFileHandler() http.HandlerFunc {
           return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          

          然后,我們使用 http.MaxBytesReader 驗證文件大小,當文件大小大于設定值時它將返回一個錯誤。錯誤將被一個助手程序 renderError 進行處理,它返回錯誤信息及對應的 HTTP 狀態(tài)碼。

           r.Body=http.MaxBytesReader(w, r.Body, maxUploadSize)
           if err :=r.ParseMultipartForm(maxUploadSize); err !=nil {
           renderError(w, "FILE_TOO_BIG", http.StatusBadRequest)
           return
           }
          

          如果文件大小驗證通過,我們將檢查并解析表單參數(shù)類型和上傳的文件,并讀取文件。在本例中,為了清晰起見,我們不使用花哨的 io.Reader 和 io.Writer 接口,我們只是簡單的將文件讀取到一個字節(jié)數(shù)組中,這點我們后面會寫到。

           fileType :=r.PostFormValue("type")
           file, _, err :=r.FormFile("uploadFile")
           if err !=nil {
           renderError(w, "INVALID_FILE", http.StatusBadRequest)
           return
           }
           defer file.Close()
           fileBytes, err :=ioutil.ReadAll(file)
           if err !=nil {
           renderError(w, "INVALID_FILE", http.StatusBadRequest)
           return
           }
          

          現(xiàn)在我們成功的驗證了文件的大小,并且讀取了文件,接下來我們該檢驗文件的類型了。一種廉價但是并不安全的方式,只檢查文件擴展名,并相信用戶沒有改變它,但是對于一個正式的項目來講不應該這么做。

          幸運的是,Go 標準庫提供給我們一個 http.DetectContentType 函數(shù),這個函數(shù)基于 mimesniff 算法,只需要讀取文件的前 512 個字節(jié)就能夠判定文件類型。

           filetype :=http.DetectContentType(fileBytes)
           if filetype !="image/jpeg" && filetype !="image/jpg" &&
           filetype !="image/gif" && filetype !="image/png" &&
           filetype !="application/pdf" {
           renderError(w, "INVALID_FILE_TYPE", http.StatusBadRequest)
           return
           }
          

          在實際應用程序中,我們可能會使用文件元數(shù)據(jù)做一些事情,例如將其保存到數(shù)據(jù)庫或將其推送到外部服務——以任何方式,我們將解析和操作元數(shù)據(jù)。這里我們創(chuàng)建一個隨機的新名字(這在實踐中可能是一個UUID)并將新文件名記錄下來。

           fileName :=randToken(12)
           fileEndings, err :=mime.ExtensionsByType(fileType)
           if err !=nil {
           renderError(w, "CANT_READ_FILE_TYPE", http.StatusInternalServerError)
           return
           }
           newPath :=filepath.Join(uploadPath, fileName+fileEndings[0])
           fmt.Printf("FileType: %s, File: %s\n", fileType, newPath)
          

          馬上就大功告成了,只剩下一個關鍵步驟-寫文件。如上文所提到的,我們只需要復制讀取的二進制文件到一個新創(chuàng)建的名為 newFile 的文件處理程序里。

          如果所有部分都沒問題,我們給用戶返回一個 SUCCESS 信息。

           newFile, err :=os.Create(newPath)
           if err !=nil {
           renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
           return
           }
           defer newFile.Close()
           if _, err :=newFile.Write(fileBytes); err !=nil {
           renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
           return
           }
           w.Write([]byte("SUCCESS"))
          

          這樣可以了. 你可以對這個簡單的例子進行測試,使用虛擬的文件上傳 HTML 頁面,cURL 或者工具例如 postman[1]

          這里是完整的代碼示例 這里[2]

          結論

          這是又一個證明了 Go 如何允許用戶為 web 編寫簡單而強大的軟件,而不必像處理其他語言和生態(tài)系統(tǒng)中固有的無數(shù)抽象層。

          在接下來的篇幅中,我將展示一些在我第一次使用 Go 語言編寫正式的 web 應用中其他細節(jié),敬請期待。;)

          // 根據(jù) reddit 用戶 lstokeworth 的反饋對部分代碼進行了修改。謝謝:)

          資源

          完整代碼示例[3]


          via: https://zupzup.org/go-http-file-upload-download/ 作者:zupzup[4] 譯者:fengchunsgit[5] 校對:polaris1119[6]

          本文由 GCTT[7] 原創(chuàng)編譯,Go 中文網(wǎng)[8] 榮譽推出

          References

          [1] postman: https://www.getpostman.com/

          [2] 這里: https://github.com/zupzup/golang-http-file-upload-download

          [3] 完整代碼示例: https://github.com/zupzup/golang-http-file-upload-download

          [4] zupzup: https://zupzup.org/about/

          [5] fengchunsgit: https://github.com/fengchunsgit

          [6] polaris1119: https://github.com/polaris1119

          [7] GCTT: https://github.com/studygolang/GCTT

          [8] Go 中文網(wǎng): https://studygolang.com/

          文是 InfoQ“解讀 2020”年終技術盤點系列文章之一。

          在作者去年年底撰寫《解讀Go語言的2019》的時候,絕沒有想到 2020 年將會如此的不平凡。全球范圍內的疫情在大大地限制了人們和企業(yè)的對外活動之余,還帶來了一個副作用,即:線下活動向線上的迅速遷移。

          實際上,對于這種遷移,我們國內的民營企業(yè)和事業(yè)單位早就在做了,只不過在 2020 年之前還沒有這么急迫。不知道你發(fā)現(xiàn)了沒有,在 2020 這一年,那些已經存在的遠程辦公、視頻會議、在線醫(yī)療、在線教育等方面的基礎設施和應用程序給予了我們莫大的支撐。即便說它們輔助保障了社會的正常運轉,也不為過。

          目前來看,全球的疫情還會存在一段時間。雖然這個事件本身絕對不值得高興,但是反過來想,這會倒逼國內數(shù)字經濟的大踏步前進,甚至飛躍。

          從基礎層面講,數(shù)字經濟的發(fā)展必須要有半導體等高精尖領域的強力支持。而從應用層面說,數(shù)字經濟將會依托于云計算、大數(shù)據(jù)和人工智能。更具體地說,云計算是高級的基礎設施,大數(shù)據(jù)和人工智能是建立在云計算之上的高級應用。Go 語言,早已霸占了云計算的大半個江山,今后它也將在大數(shù)據(jù)和人工智能方面發(fā)揮重要作用。

          趨勢縱覽

          下面,我們依舊先從整體趨勢上看看 Go 語言在今年的發(fā)展。

          在全球范圍內,從 2010 年的集體追新,到之后幾年內的理性對待,再到 2016 年、2017 年的“第二春”,直至 2018 年的升降大反差和 2020 年的新反彈。Go 語言可謂是經歷了諸多風風雨雨,持續(xù)地在各種好評和詬病之間砥礪前行,既得意過也失意過。

          下圖展現(xiàn)了 TIOBE Index(著名編程語言排行榜)對 Go 語言使用情況的最新統(tǒng)計。

          圖 1 - TIOBE Index 之 Go 語言(2020 年 12 月)

          ? 圖 2 - TIOBE Index(2020 年 12 月)

          我們從上面這兩幅圖中可以看出, Go 語言在今年的排名又有了大幅的提升。作者個人認為,這與 go mod 工具的轉正和推廣,以及“泛型”實現(xiàn)的排期確定是分不開的。

          同時,據(jù) StackOverflow(全球最大的編程社區(qū)和問答網(wǎng)站)在前不久發(fā)布的一份開發(fā)者生存報告顯示,Go 語言在 2020 年是繼 Python、Java、C++和 C 之后、排名第五的通用型、全平臺編程語言。如果把腳本語言和標記語言都算在內的話,它的總排名是第 12 名。

          圖 3 - Stack Overflow Servey 2020 - The Most Popular Languages

          不但如此,Go 語言在“最喜愛”和“最需要”的編程語言排行中也名列前茅。

          ? 圖 4 - Stack Overflow Servey 2020 - The Most Loved Languages

          圖 5 - Stack Overflow Servey 2020 - The Most Wanted Languages

          我們可以看到,Go 語言不但是開發(fā)者們非常喜愛的編程語言之一(“最喜愛”排行榜第五名),而且從實際應用的角度看,大家也是非常需要它的(“最需要”排行榜第三名)。作者認為,正因為 Go 語言有著崇尚簡約和實用主義的編程哲學,廣大軟件工程師才會如此地愛用它。

          更重要的是,Go 軟件工程師的薪資待遇也是相當不錯的。

          圖 6 - Stack Overflow Servey 2020 - The Highest Salaries

          你可能會奇怪,為什么 Perl 程序員的薪資排在了第一位?這可能是因為物以稀為貴,Perl 程序員在當代已經非常少見了。而在當今很熱門的通用型編程語言中,從薪資角度來看,Scala 語言、Go 語言和 Rust 語言都有著相當大的優(yōu)勢。

          當然了,這是在全球范圍內的情況,并且參與這份調查的中國開發(fā)者并不多。很可惜,作者沒能找出一份公認且權威的國內開發(fā)者調查報告。

          不過,從作者的親身經歷來看,Go 語言在國內恐怕并不亞于國際上的熱度,甚至還要更火熱一些。

          作者這兩年一直在斷斷續(xù)續(xù)地幫助一些互聯(lián)網(wǎng)企業(yè)招聘 Go 軟件工程師。除了作為老一代霸主的 BAT(百度、阿里巴巴、騰訊)以及作為新一代翹楚的 TMD(、美團、滴滴)之外,還有很多知名的互聯(lián)網(wǎng)公司都在招聘掌握 Go 語言的開發(fā)工程師和系統(tǒng)運維人員。像 PingCAP、七牛、嗶哩嗶哩、探探、Grab 這些公司,在很早以前就混跡于 Go 語言圈子了。而在最近幾年才進入 Go 語言圈子的知名公司還有華為、小米、映客、云智聯(lián)、輕松籌、貝殼網(wǎng)、美菜網(wǎng)、游族網(wǎng)絡等等。就連剛開始大紅大紫的工業(yè)互聯(lián)網(wǎng)領域,也有不少公司選擇 Go 語言作為其主力開發(fā)語言之一。比如,積夢智能、必可測等。

          這么多的優(yōu)秀企業(yè),以及活躍在技術社區(qū)中的大佬和新秀共同營造出了 Go 語言工程師的供需網(wǎng)絡。作者認為,在國內的服務端編程市場,除了 Java 和 PHP,就當屬 Go 語言了。

          2020 年回顧

          在了解了 Go 語言的發(fā)展趨勢之后,我們再一起來看看它在 2020 年都有哪些重要的更新。

          模塊:終于穩(wěn)定

          自 2020 年 2 月份發(fā)布的 1.14 版本起,Go 語言官方就開始正式地推廣 go modules 了。這說明它已經完全可以在生成環(huán)境中使用了。

          如果你是老牌的 Go 工程師的話,那么肯定使用過像 glide、dep、govendor 這類第三方的依賴管理工具。這些工具都非常的優(yōu)秀,并且在很大程度上解決了我們在項目開發(fā)過程中遇到的痛點。

          不過,現(xiàn)在是時候遷移到 go modules 了。Go modules 綜合了這些第三方工具的優(yōu)點,并經歷了數(shù)年的設計和磨合,最終成為了 Go 程序依賴管理的官方工具。

          即使現(xiàn)存的項目已經使用了前面提及的某一個依賴管理工具,那么也無需擔心。我們只需要在 Go 項目的根目錄中運行命令“go mod init <項目主模塊的導入路徑>”就可以實現(xiàn)自動地遷移。go mod 命令會讀取那些已經存在的依賴配置文件,然后在其創(chuàng)建的 go.mod 文件中添加相應的內容。不過在這之后,我們最好再次使用 go build 命令構建一下項目并運行相應的單元測試,以確保一切正常。

          還記得系統(tǒng)環(huán)境變量 GOPATH 嗎?現(xiàn)在的 go 命令會自動地把項目所需的依賴包下載到它指向的第一個工作區(qū)目錄中的 pkg/mod 子目錄里。這里有一點需要注意,如果我們的項目中存在處于頂層的 vendor 目錄,那么 go 命令將會優(yōu)先在該目錄中查找對應的依賴包。

          如果我們使用的是 Go 語言的 1.15 版本,那么也可以通過設置系統(tǒng)環(huán)境變量 GOMODCACHE 來自定義上述存儲依賴包的目錄。這實際上是為以后徹底廢棄 GOPATH 埋下的一個伏筆。

          另外,執(zhí)行一下 go mod tidy 命令也是一個很好的主意。這個命令會對 go modules 的依賴配置文件進行整理,添加那些實際在用的依賴項,并去除那些未用的依賴項。換句話說,它會確保項目的依賴配置文件與項目源碼的實際依賴相對應。

          Go 語言的大多數(shù)標準命令都得到了不同程度的改進以更加適配 go modules,包括一些標記(flag)的調整和一些行為上的優(yōu)化。比如,go get 命令在默認情況下不再會去更新非兼容版本的依賴庫。不兼容的依賴庫更新常常會讓我們很惱火,但現(xiàn)在不會再出現(xiàn)這種情況了。

          環(huán)境變量:跟進的調整

          Go 語言可識別的系統(tǒng)環(huán)境變量 GO111MODULE 在 1.14 和 1.15 版本中的默認值都是 auto。這意味著,go 命令僅在當前目錄或上層目錄中存在 go.mod 文件的情況下才會以 go modules 的方式運行,否則它就會退回到之前以 GOPATH 為中心的運行方式。不過,預計在明年發(fā)布的 1.16 版本中,Go 語言將會把這個環(huán)境變量的默認值設置為 on。也就是說,到了那時,GOPATH 這一古老但能勾起我們滿滿回憶的東西終于要默默地退出了。

          另外,我們現(xiàn)在可以在系統(tǒng)環(huán)境變量 GOPROXY 的值中使用管道符“|”了。在這之前,GOPROXY 的值中只能出現(xiàn)分隔符“,”。如果一個代理 URL 跟在了分隔符后面,那么只有在前一個代理 URL 指向的服務器返回 404 或 401 錯誤時,go 命令才會嘗試使用當前的代理 URL。現(xiàn)在,如果一個代理 URL 跟在了管道符后面,那么只要在訪問前一個服務器時發(fā)生了(任何的)錯誤,go 命令就會馬上使用當前的代理 URL。換句話說,新的管道符讓我們多了一種容錯的選擇,即范圍更廣的容錯。合理使用它,可以讓我們更快地從可用的代理那里下載到所需的代碼包。

          順便說一下,我們現(xiàn)在有了一個新的系統(tǒng)環(huán)境變量 GOINSECURE。這個環(huán)境變量的存在單純是為了讓我們能夠從非 HTTPS 的代碼包服務器上下載依賴包(或者模塊)。

          有關環(huán)境變量的更多細節(jié),我就不在這里說了。大家如果想了解的話,可以去參看 Go 語言的相關文檔。

          語言語法:可重疊的接口方法

          我們都知道,Go 語言這些年在語法方面一向很穩(wěn)定,少有改動,更沒有不兼容的變化出現(xiàn)。在 2020 年,Go 語言只做了一項語法改進。這是關于接口聲明的,并且完全保證了向后兼容性。我們下面來看一組代碼示例。假設,我們有如下兩個接口聲明:

          // MyReader 代表可讀的自定義接口。
          type MyReader interface {
              io.ReadCloser
          }
          // MyWriter 代表可寫的自定義接口。
          type MyWriter interface {
              io.WriteCloser
          }
          
          

          在 Go 1.14 之前,這兩個接口是無法內嵌到同一個接口聲明中去的。就像這樣:

          // MyIO 代表可輸入輸出的自定義接口。
          type MyIO interface {
              MyReader
              MyWriter
              io.Closer
          }
          
          

          這會讓 Go 語言的編譯器報錯。報錯的原因是:在同一個接口聲明中發(fā)現(xiàn)了重復的方法聲明。更具體地說,Go 語言標準庫中的 io.ReadCloser 接口和 io.WriteCloser 接口都包含了一個名為 Close 的方法,而分別內嵌了這兩個接口的 MyReader 和 MyWriter 又已經嵌入到了接口 MyIO 之中。這導致 MyIO 接口里現(xiàn)在存在兩個 Close 方法的聲明。所以,MyIO 的聲明是無效的,并且無法通過編譯。

          這看上去是合規(guī)的,但卻不一定合理。因為在很多情況下,我們想做的只是把多個接口合并在一起,而不在乎方法聲明是否有重疊。我們一般認為,如果有重疊的方法,那么就當作一個就好了。很可惜,之前的 Go 語言并不這么認為。更重要的是,對于像上面那樣深層次的接口內嵌問題,我們排查和解決起來都會很麻煩。有時候,這還會涉及到第三方庫。

          值得慶幸的是,自 Go 1.14 開始,我們的這個合理訴求終于得到了滿足。Go 語言的語法已經認可了上述情況。這將給我們的接口整合工作帶來極大的便利。

          不過請注意,Go 語言只接受在同一個接口聲明中完全重疊的多個方法聲明。換句話說,只有這些方法聲明在名稱和簽名上完全一致,它們才能夠合而為一。別忘了,接口中方法的簽名包括了參數(shù)列表和結果列表。如果僅名稱相同但簽名不同,那么 Go 語言編譯器照樣會報錯。例如:

          // MyIO 代表可輸入輸出的自定義接口。
          type MyIO interface {
              MyReader
              MyWriter
              Close()
          }
          
          

          由于這里最后面的方法聲明 Close() 與接口 io.ReadCloser 和 io.WriteCloser 中的方法聲明 Close() error 不完全一致(請注意結果聲明上的差異),所以 Go 語言仍然會報出錯誤“duplicate method Close”,并使得程序編譯不通過。

          運行時內部:性能提升

          Go 語言每次的版本更新都會包含針對其運行時系統(tǒng)的性能提升。在 2020 年的優(yōu)化中,有幾點值得我們注意:

          1. goroutine 真正實現(xiàn)了異步的搶占。也就是說,現(xiàn)在即使是不包含任何函數(shù)調用的 for 循環(huán)也絕不會引起程序的死鎖和垃圾回收的延誤了。

          2. defer 語句的執(zhí)行效率又得到了進一步的提升,額外的開銷已幾乎為零。所以,我們已經完全可以將 defer 語句用在對性能有嚴苛要求的應用場景中了。

          3. 運行時系統(tǒng)內部的內存分配器獲得了改進。這使得在系統(tǒng)環(huán)境變量 GOMAXPROCS 有較高數(shù)值的情況下,內存的申請效率會得到大幅的提升。這間接地讓運行時系統(tǒng)的整體性能明顯提高。

          除了以上這些,Go 語言運行時系統(tǒng)還有一些比較小的改進,比如:panic 函數(shù)已經可以正確地打印出我們傳入的參數(shù)值當中的各種數(shù)值了、在將較小的整數(shù)值轉換為接口值的過程中不再會引起內存分配、針對通道的非阻塞接收操作得到了進一步的優(yōu)化,等等。

          并發(fā)編程:一些微調

          我們都知道,runtime.Goexit 函數(shù)在被調用之后會中止當前的 goroutine 的運行。然而,在嵌套的 panic/recover 處理流程中,程序對它的調用會被忽略掉。雖然這種應用場景非常少見,但終歸是一個問題。幸好自 Go 1.14 開始,這個問題被徹底地解決了。

          還要注意,如果 runtime.Goexit 函數(shù)被主 goroutine 中的代碼調用了,那么它并不會終止整個 Go 程序的運行,而只會中止主 goroutine 而已。在這種情況下,其他的 goroutine 會繼續(xù)運行。如果,在這之后,這些其他的 goroutine 都運行完畢了,那么程序就會崩潰。所以說,我們千萬不要在主 goroutine 中調用 runtime.Goexit 函數(shù)。

          在同步工具方面,現(xiàn)在的 Go 運行時系統(tǒng)會更加關注那些競爭非常激烈的互斥鎖。一旦這樣的鎖被解鎖,系統(tǒng)會立即安排 CPU 去運行正在等待該鎖的下一個 goroutine(也就是說,它會跳過調度的過程)。這顯然可以大大提高此類場景下的互斥鎖性能。

          并發(fā)安全字典 sync.Map 有了一個新方法 LoadAndDelete。從方法名上我們也可以看得出來,這個方法能夠在獲取某個元素值的同時把它從字典中刪除掉。更重要的是,Go 語言會保證這個“獲取+刪除”的操作的原子性。另外,該方法返回的第二個結果值會告知我們,字典先前是否包含與我們傳入的健值(key)對應的元素值(element)。如果不包含,那么它返回的第一個結果值就會是 nil。

          再來說 context.Context。雖然它并不在 sync 代碼包中,但是我們常常用它來做上下文的同步。因此,它也算是一個很重要的同步工具。關于它的調整很簡單也很明確,那就是:不再允許使用 nil 作為父級的上下文來創(chuàng)建衍生的 Context 實例了。這一要求顯然是合理的,因為所謂的衍生 Context 就應該有實實在在的父級。然而,在 Go 1.15 之前,并沒有專門的防衛(wèi)語句來對此進行前置檢查。此項改進涉及到了 context 包中的幾個重要函數(shù),如 WithCancel、WithDeadline、WithTimeout 和 WithValue。現(xiàn)在,如果我們傳給這些方法的第一個參數(shù)值是 nil,那么它們都將會立即拋出包含了對應錯誤信息的 panic。

          標準庫:新的散列算法包

          標準庫中多了一個新的代碼包:hash/maphash。這是一個通用的散列算法包,可以將任意的字節(jié)序列或者字符串散列成 64 位的整數(shù)。從名字上我們也可以看出,它能夠幫助我們實現(xiàn)那些基于散列表的數(shù)據(jù)結構。該包中的 Hash 類型的基本行為如下:

          1. 此 Hash 類型是開箱即用的。也就是說,我們在實例化它的時候無需額外的初始化工作,僅創(chuàng)建一個它的零值即可使用。

          2. 不同的 Hash 實例在默認的情況下會有不同的默認種子。因此,這些默認實例為同一個對象計算出的散列值將會不同。

          3. Hash 實例允許手動設置種子(必須由 MakeSeed 函數(shù)產生)。在單一進程中,只要種子相同,Hash 實例為同一個對象計算出的散列值就會相同。不論進行計算的 Hash 實例是一個還是多個,都會如此。

          4. 被計算的對象的表現(xiàn)形式可以是字節(jié)序列,也可以是字符串。只要內容一致,不論它們是以怎樣的方式寫入 Hash 實例的,計算出的散列值都會相同(大前提是進程和種子都相同)。

          5. Hash 實例可以被重置。此操作會清空已寫入當前 Hash 實例的內容,但并不會改變當前的種子。

          總之,hash/maphash.Hash 類型代表著一種基于種子的、可重用、可重置、穩(wěn)定、通用的散列算法。這種算法的優(yōu)勢是可以有效地避免散列沖突,但它并不是加密安全的。如果你的關注點是加密安全,那么可以考慮使用標準庫 crypto 包中的算法包,如 md5、sha256、sha512 等。

          單元測試:資源清理

          今年,Go 語言還在單元測試方面做了一些改進。

          作者認為最實用的一項改進當屬 Cleanup 方法的增加。更具體地說,現(xiàn)在 testing.T 類型和 testing.B 類型都有了這個方法。該方法接受一個函數(shù)作為其參數(shù)值。我們的測試代碼可以多次調用 Cleanup 方法,以傳入多個函數(shù)。這些函數(shù)都會被記錄在測試程序的內部。當測試即將結束的時候,這些函數(shù)就會被一一調用,且越晚傳入的函數(shù)會越先被調用。

          雖然 Cleanup 方法的文檔中并沒有規(guī)定這些被傳入的函數(shù)應該做什么。但正如其名,它們最應該做的是清理在測試運行的過程中用到的各種資源。

          另外,testing.T 類型和 testing.B 類型還各自多了一個名為 TempDir 的方法。這個方法會返回一個臨時目錄的路徑。這樣的臨時目錄是專門為當前的測試實例創(chuàng)建的。并且,這些目錄會在測試即將結束的時候被自動地刪除掉。因此,我們可以在測試的過程中根據(jù)實際需要在這樣的目錄下創(chuàng)建一些臨時的文件,以幫助測試更好的進行。

          時區(qū)信息:獨立性增強

          Go 1.15 包含了一個新的代碼包 time/tzdata。該包允許將時區(qū)數(shù)據(jù)庫嵌入到 Go 程序當中。當我們在程序中添加導入語句(即 import _ "time/tzdata")之后,即使本地系統(tǒng)里不存在時區(qū)數(shù)據(jù)庫,當前程序也完全可以正確地查詢到時區(qū)信息。另外,我們還可以通過在構建程序時追加 -tags timetzdata 來嵌入時區(qū)數(shù)據(jù)庫。當然了,天下沒有免費的午餐。這兩種方法都會使 Go 程序的大小增加大約 800 KB(預計在 Go 1.16 中會降低到 350 KB)。

          明年的展望

          目前,可以明確的是,Go 語言明年肯定不會有泛型和新的錯誤處理機制。如果不出意外的話,范型應該會在 2022 年發(fā)布的 1.18 版本中出現(xiàn)。而有些可惜的是,錯誤處理方面的變革可能要等到 Go 2 發(fā)布的時候才能夠真正的實現(xiàn)。

          關于 Go 語言的泛型,大家可以參看 Ian Lance Taylor 和 Robert Griesemer 在 2020 年 11 月 25 日發(fā)布的最新設計草案。而對于新的錯誤處理機制,大家可以去看Go 2設計草案中“Error handling”部分。作者就不在這里多說了。

          我們下面來看看,將要在 2021 年 2 月發(fā)布的 Go 1.16 預計會包括哪些重要的更新。由于 1.16 版本目前還在開發(fā)當中,因此以下所講的內容不一定就是最終的實現(xiàn)。

          端口:支持新的組合

          大家都知道,蘋果公司已經發(fā)布了他們自己研發(fā)的 CPU,并且已經應用在了自家的入門級電腦當中。然而,很多在 macOS 操作系統(tǒng)上運行的軟件卻還沒有準備好。Go 語言也在其列。

          不過,Go 語言打算從 1.16 版本開始支持這種新的“計算架構+操作系統(tǒng)”組合,代號為“darwin/arm64”。請注意,在當下的 Go 語言之中其實已經存在了“darwin/arm64”,只不過現(xiàn)在這個代號實際上對應的是“ARM 處理器+iOS 操作系統(tǒng)”。為了避免混淆,之前的“darwin/arm64”將更名為“ios/arm64”,而 “darwin/arm64”之后將對應于“ARM 處理器+macOS 操作系統(tǒng)”。

          模塊與工具:GOPATH 即將下崗

          我們在前文說過,Go 語言預計在 1.16 版本將系統(tǒng)環(huán)境變量 GO111MODULE 的默認值改為 on。這就意味著,GOPATH 以及以它為中心的程序存儲和構建方式終于要向我們揮手告別了。不過,若我們還想讓 go 命令以之前的方式運行,那么還可以把該環(huán)境變量的值改成 auto。按照慣例,本文作者估計這個環(huán)境變量還會再在 Go 語言中留存 1 或 2 個版本。

          與之相應的,Go 語言自帶的各種標準工具會徹底地站在 go modules 一方。

          命令 go install 將會支持安裝指定版本號的代碼包。更具體地說,我們在輸入命令的時候可以這樣:“go install golibhub.com/mylib@v1.2.1 ”。在這種情況下,go 命令將會忽略掉相關 go.mod 文件中的 mylib 包條目,即使那里配置的版本號與命令參數(shù)中的不同也會如此。順便說一下,Go 語言官方將會在之后的某個版本中改變 go get 命令的行為,使它只負責下載代碼包,而不再自動地進行代碼包的構建和安裝。也就是說,這個“構建+安裝”的動作將只由 go install 命令負責。

          另外,go build 命令和 go test 命令將不會再對 go.mod 文件進行任何的修改。對它們來說,go.mod 文件將會是只讀的。一個相應的行為是,如果這兩個命令在執(zhí)行的過程中發(fā)現(xiàn)需要對依賴配置文件進行修改(或者說有必要調整依賴包的配置信息),那么它們將會立即報錯。這與之前在輸入命令時追加標記 -mod=readonly 的行為是一致的。這時,我們可以使用命令 go mod tidy 或 go get 來做相應的調整。

          運行時 API:監(jiān)測的增強

          在 Go 1.16 中,將會出現(xiàn)一個新的用于運行時度量的代碼包 runtime/metrics。該代碼包旨在引入一種穩(wěn)定的度量接口,用于從 Go 運行時系統(tǒng)中讀取相應的指標數(shù)據(jù)。它在功能上會取代現(xiàn)有的諸如 runtime.ReadMemStats、debug.GCStats 等 API,并且會更加的通用和高效。

          另外,系統(tǒng)環(huán)境變量 GODEBUG 將可以接受一個新的選項“inittrace”。當該環(huán)境變量的值中包含“inittrace=1”的時候,Go 運行時系統(tǒng)會輸出有關于 init 函數(shù)的監(jiān)測信息。其中會包含對應函數(shù)的執(zhí)行時間和內存分配情況。這將非常有利于我們觀察各個代碼包在初始化方面的性能。

          標準庫:新的嵌入包

          Go 1.16 的標準庫中將會出現(xiàn)一個新的代碼包 embed。這個包的主要作用是讓 go 命令在編譯程序的時候向其嵌入指定的外部文件。至于嵌入什么文件,需要我們通過注釋指令 //go:embed 來指定,如://go:embed hello.txt 。請注意,在包含了這個注釋指令的源碼文件中必須要有針對 embed 包的導入語句,如:import "embed" 。

          同時,這樣的注釋指令必須緊挨在單一變量聲明語句的上方,且該變量的類型必須是 string、[]byte 或 embed.FS。這樣的話,Go 語言就會自動地把我們指定的文件的內容轉化為相應的值,并賦給這個變量。

          在 //go:embed 的右邊,我們可以用空格來分割多個文件路徑,或者通過添加多個這樣的注釋指令來分別指定多個文件。文件的路徑可以是相對的,也可以是絕對的

          另外,這里的文件路徑還可以包含通配符,如://go:embed image/* template/* 。具體有哪些通配符可用,大家可以去參看 path.Match 函數(shù)的文檔。

          一定要注意,如果需要嵌入多個文件,那么我們就必須把變量的類型聲明為 embed.FS。這主要是因為,只有這個類型才能把多個嵌入文件的內容區(qū)分開來。通過該類型的方法(Open、ReadDir 和 ReadFile),我們還可以分別拿到代表了某個文件或目錄的實體,或者讀取其中任何文件的內容。

          究其原因,結構體類型 embed.FS 的實例可以代表一個樹形的文件系統(tǒng)。也就是說,它可以用來表示一個擁有多個層級的文件目錄。另外,embed.FS 類型是 io/fs.FS 接口的一個實現(xiàn),因此它的實例可以被應用在很多理解統(tǒng)一文件系統(tǒng)的 API 之上。這些 API 散落在 net/http、text/template、html/template 等代碼包之中。

          請想象一下,如果我們在開發(fā)一個帶有 Web 頁面和靜態(tài)資源的軟件系統(tǒng),那么這將會給我們帶來多么大的便利。讓單個的可執(zhí)行文件包含所有的程序和資源將變?yōu)榭赡堋?/p>

          標準庫:新的文件系統(tǒng)包

          代碼包 io/fs 代表了一種全新的文件系統(tǒng)模型。它可以對應于任意(已支持的)操作系統(tǒng)中的文件系統(tǒng),但并不局限于此。該包中的核心就是我們已經在前面提及的 FS 接口,以及還未講到的 File 接口。

          簡單來說,F(xiàn)S 接口代表了一個文件系統(tǒng)抽象的最小實現(xiàn)要求。其中只有一個方法聲明:Open(name string) (File, error) 。而 File 接口則代表了可以在單個文件上進行的一系列操作。請看下面這幅圖。

          ? 圖 7 - io/fs 包中的接口

          到目前為止,這個代碼包中的接口共有 10 個。其他的大部分接口都會內嵌 FS 接口或 File 接口。也就是說,它們都屬于核心接口的擴展。雖然從聲明上看 DirEntry 接口和 FileInfo 接口是獨立的,但它們都被多個其他的接口所引用。

          在仔細閱讀上圖或者該包的源碼之后,你一定會發(fā)現(xiàn),這個模型所代表的文件系統(tǒng)是只讀的。也就是說,其中并沒有表示“寫操作”的方法。

          正因為這個模型可以用來表示任何樹形結構的資源系統(tǒng),所以作為統(tǒng)一的模型,它只提供了最基礎的抽象。要知道,有的資源系統(tǒng)就是只能讀、不能寫的。比如,我們前面說過的嵌入 Go 程序的外部文件和目錄就必須是只讀的。也就是說,它們在嵌入程序之后就不應該再被改變了。

          為了適配這個模型,Go 語言標準庫中的不少代碼包都做了相應的調整,比如:os 包、net/http 包、archive/zip 包,以及 html/template 包和 text/template 包。

          這里有一些更具體的例子:

          • embed.FS類型用于表示嵌入的文件或目錄。它實現(xiàn)了io/fs包中的FS接口、ReadDirFS接口和ReadFileFS接口;
          • 新的os.DirFS函數(shù)可以提供由當前的操作系統(tǒng)支持的io/fs.FS接口實現(xiàn);
          • 新的http.FS函數(shù)和http.FileServer函數(shù)可以把io/fs.FS接口的實例包裝成http.Handler;
          • 新的testing/fstest包專門用于測試相應的文件系統(tǒng)模型,其中還包含了一個基于內存的文件系統(tǒng)抽象MapFS。

          總之,代碼包 io/fs 代表了一個全新的、統(tǒng)一的文件系統(tǒng)(或者說資源系統(tǒng))的模型。這個模型將會作為很多具體應用的底層框架。另外,io/fs 包在以后可能會得到進一步的拓展,說不定還會發(fā)展出一些描述“寫操作”的擴展接口。作者也希望如此。

          總結

          好了,我們現(xiàn)在來稍微總結一下。在 2020 年,Go 語言同樣做出了很多改變。這包括已經完全穩(wěn)定的 go modules、環(huán)境變量和標準工具的跟進和增強、語法上的一項重要調整——可重疊的接口方法、運行時系統(tǒng)的性能提升、異步編程和同步工具方面的進一步優(yōu)化,以及新的散列算法包、新的單元測試輔助方法和獨立的時區(qū)代碼包。這些更新表面上看起來可能并不算大,但 Go 語言內部其實已經做了很多的改變。有的改變是完全的,而有的改變是在為以后的目標做鋪墊。


          我們對 Go 語言有著很多的期望。但是,大餅還是要一口一口的吃。在 2021 年,Go 語言中的 GOPATH 將會正式宣布下崗。同時,Go 語言也會把一些重要的東西統(tǒng)一起來,比如用于運行時度量的代碼包 runtime/metrics,以及代表了新的文件系統(tǒng)模型的代碼包 io/fs。隨著 io/fs 包而來的,還有便于我們將程序和資源整合成一個單獨文件的代碼包 embed 和注釋指令 //go:embed 。另外,作者也非常期待 Go 語言對“ARM+macOS”組合的支持。


          目前可以預計,Go 語言的泛型支持將會比新的錯誤處理機制更早到來。不過,Go 語言官方在保證向后兼容性的情況下已經對現(xiàn)有的錯誤處理 API 進行了盡可能的改進。更何況,我們還有幾個不錯的錯誤處理包可用,如官方的擴展包 golang.org/x/xerrors,以及已經過時間驗證的第三方包 github.com/pkg/errors 和 gopkg.in/errgo.v2。所以當前看來,這個問題早已不那么尖銳了。


          作者對 Go 語言的發(fā)展仍然是非常樂觀的,尤其是在“云原生”大行其道的當下。在數(shù)據(jù)科學方面,七牛云的 CEO 許式偉正在帶頭創(chuàng)造對標“Python 語言+Numpy+Pandas”和(MIT 出品的)Julia 語言的新玩意兒——基于 Go 語言的編程語言Go+。作者對此還是很看好的。不過,據(jù)說(Apple 出品的)Swift 語言也將在這一領域繼續(xù)發(fā)力,可能會出現(xiàn)“Swift Number”之類的東西,同樣對標基于 Python 語言的數(shù)據(jù)科學包。作者相信,到了 2021 年下半年或者 2022 年,這里很可能會出現(xiàn)四足鼎立的態(tài)勢。總之,有競爭才會有突破,基于 Go 語言的生態(tài)環(huán)境依然不可限量。

          示例代碼

          github.com/hyper0x/go2020

          延伸閱讀

          解讀 2015 之 Golang 篇:Golang 的全迸發(fā)時代

          解讀 2016 之 Golang 篇:極速提升,逐步超越

          Go 語言的 2017 年終總結

          解讀 2018 之 Go 語言篇(上):為什么 Go 語言越來越熱?

          解讀 2018 之 Go 語言篇(下):明年有哪些值得期待?

          解讀Go語言的2019:如果驚喜不再 還有哪些值得關注?


          參考資料

          Go 語言官方的源碼和文檔

          Go 1.14 Release Notes: https://golang.org/doc/go1.14

          Go 1.15 Release Notes: https://golang.org/doc/go1.15

          Go 1.16 Release Notes(DRAFT): https://tip.golang.org/doc/go1.16


          作者簡介

          郝林,國內知名的編程布道者,技術社群 GoHackers 的發(fā)起人和組織者。他發(fā)布過很多 Go 語言技術教程,包括開源的《Go命令教程》、極客時間的付費專欄《Go語言核心36講》,以及圖靈原創(chuàng)圖書《Go并發(fā)編程實戰(zhàn)》,等等。其中的專欄和圖書都有數(shù)萬的訂閱者或購買者,而那個開源教程的 star 數(shù)也有數(shù)千。另外,在 2020 年,他還出版了一本新書《Julia編程基礎》。這本書主要面向的是廣大的編程初學者,以及對函數(shù)式編程和數(shù)據(jù)科學感興趣的軟件開發(fā)者。


          關注我并轉發(fā)此篇文章,私信我“領取資料”,即可免費獲得InfoQ價值4999元迷你書,點擊文末「了解更多」,即可移步InfoQ官網(wǎng),獲取最新資訊~

          Go語言實際開發(fā)中,會遇到讀取、寫入或編輯docx格式文件等情況,目前使用比較多的第三方庫有

          源碼:

          • github.com/unidoc/unioffice
          • github.com/carmel/gooxml
          • github.com/nguyenthenguyen/docx

          unidoc/unioffice 功能比較齊全,但是收費的,這里就不做介紹了。 carmel/gooxml: 這個庫為unidoc/unioffice免費版,即1.4.0收費版,在雙重許可證下提供的。在AGPLv3的條款下可以免費使用。雖然沒有收費版功能多,但涵蓋了大部分基本功能。 nguyenthenguyen/docx: 是一個簡單的操作docx文件go語言庫

          carmel/gooxml

          取docx文件 示例

          package main
          
          import (
              "fmt"
              "github.com/carmel/gooxml/document"
              "log"
              "os"
              "strconv"
          )
          
          func main() {
              doc, err :=document.Open("demo.docx")
              if err !=nil {
                  log.Fatalf("error opening document: %s", err)
                  return
              }
              //批注
              for _, docfile :=range doc.DocBase.ExtraFiles {
                  if docfile.ZipPath !="word/comments.xml" { //只處理word/comments.xml
                      continue
                  }
                  file, err :=os.Open(docfile.DiskPath)
                  if err !=nil {
                      continue
                  }
                  defer file.Close()
                  f, err :=file.Stat()
                  if err !=nil {
                      continue
                  }
                  size :=f.Size()
                  var fileinfo []byte=make([]byte, size)
                  _, err=file.Read(fileinfo)
                  if err !=nil {
                      continue
                  }
                  //實際應該解析<w:t>中的數(shù)據(jù)
                  fmt.Println(string(fileinfo))
              }
          
              //書簽
              for _, bookmark :=range doc.Bookmarks() {
                  bookname :=bookmark.Name()
                  if len(bookname)==0 {
                      continue
                  }
                  fmt.Println(bookmark.Name())
              }
          
              //頁眉
              for _, head :=range doc.Headers() {
                  var text string
                  for _, para :=range head.Paragraphs() {
                      for _, run :=range para.Runs() {
                          text +=run.Text()
                      }
                  }
                  if len(text)==0 {
                      continue
                  }
                  fmt.Println(text)
              }
          
              //頁腳
              for _, footer :=range doc.Footers() {
                  for _, para :=range footer.Paragraphs() {
                      var text string
                      for _, run :=range para.Runs() {
                          text +=run.Text()
                      }
                      if len(text)==0 {
                          continue
                      }
                      fmt.Println(text)
                  }
              }
              //輸出圖片
              //var fileBytes []byte
              for k, img :=range doc.Images {  //返回文檔內所有圖片
                  fmt.Println("image:", k, img.Format(), img.Path(), img.Size())
              }
          
          
              //doc.Paragraphs()得到包含文檔所有的段落的切片
              for _, para :=range doc.Paragraphs() {
                  var text string
                  //run為每個段落相同格式的文字組成的片段
                  for _, run :=range para.Runs() {
                      text +=run.Text()
                      //fmt.Println("粗體", run.Properties().IsBold(), run.Text())   //判斷是否是粗體
                      //fmt.Println("粗體屬性值", run.Properties().BoldValue(), run.Text())
                      // fmt.Println("斜體", run.Properties().IsItalic(), run.Text()) //判斷是否是斜體
                      //fmt.Println("斜體屬性值", run.Properties().ItalicValue(), run.Text())
                  }
                  if len(text)==0 {
                      continue
                  }
                  //打印一段
                  fmt.Println(text)
              }
          
              //獲取表格中的文本
              for tId, table :=range doc.Tables() {
                  for rowId, run :=range table.Rows() {
                      for cellId, cell :=range run.Cells() {
                          var text string
                          for _, para :=range cell.Paragraphs() {
                              for _, run :=range para.Runs() {
                                  text +=run.Text()
                                  //fmt.Println("粗體", run.Properties().IsBold(), run.Text())   //判斷是否是粗體
                                  //fmt.Println("粗體屬性值", run.Properties().BoldValue(), run.Text())
                                  // fmt.Println("斜體", run.Properties().IsItalic(), run.Text()) //判斷是否是斜體
                                  //fmt.Println("斜體屬性值", run.Properties().ItalicValue(), run.Text())
                              }
                          }
                          if len(text)==0 {
                              continue
                          }
                          fmt.Println(text)
                          fmt.Println("table"+strconv.Itoa(tId), "行"+strconv.Itoa(rowId), "列"+strconv.Itoa(cellId))
                      }
                  }
              }
          
          }
          

          創(chuàng)建docx文件

          示例

          package main
          
          import (
              "github.com/carmel/gooxml/color"
              "github.com/carmel/gooxml/common"
              "github.com/carmel/gooxml/document"
              "github.com/carmel/gooxml/measurement"
              "github.com/carmel/gooxml/schema/soo/wml"
              "log"
          )
          
          func main() {
              doc :=document.New()
              para :=doc.AddParagraph() // 新增段落
              run :=para.AddRun()
              //設置段落
              para.SetStyle("Title")
              para.SetStyle("Heading1")  // Heading1 Heading2 Heading3
              para.Properties().SetFirstLineIndent(0.5 * measurement.Inch) // 段落添加首行縮進
          
              // 換行處理,使用'\r'
              run.AddText("這里是段落文字信息\n這里是第二行段落文字信息") // 添加文字信息
              para.Properties().AddSection(wml.ST_SectionMarkNextPage) // 另起一頁(用在AddText之后)
          
              //設置字體樣式
              run.Properties().SetBold(true)             // 是否加粗
              run.Properties().SetFontFamily("Courier")  // 字體
              run.Properties().SetSize(15)               // 字號
              run.Properties().SetColor(color.Red)       // 文字顏色
              run.Properties().SetKerning(5)             // 文字字距
              run.Properties().SetCharacterSpacing(5)    // 字符間距調整
              run.Properties().SetHighlight(wml.ST_HighlightColorYellow) // 設置高亮
              run.Properties().SetUnderline(wml.ST_UnderlineWavyDouble, color.Red) // 下劃線
          
              // 初始化圖片信息
              img1, err :=common.ImageFromFile("demo.jpg")
              if err !=nil {
                  log.Fatalf("unable to create image: %s", err)
              }
              img1ref, err :=doc.AddImage(img1)
              if err !=nil {
                  log.Fatalf("unable to add image to document: %s", err)
              }
              // 將圖片添加到對應的段落
              anchored, err :=para.AddRun().AddDrawingAnchored(img1ref)
              if err !=nil {
                  log.Fatalf("unable to add anchored image: %s", err)
              }
              // 設置圖片相關樣式
              anchored.SetName("圖片名稱")
              anchored.SetSize(2*measurement.Inch, 2*measurement.Inch)
              anchored.SetOrigin(wml.WdST_RelFromHPage, wml.WdST_RelFromVTopMargin)
              anchored.SetHAlignment(wml.WdST_AlignHCenter)
              anchored.SetYOffset(3 * measurement.Inch)
              anchored.SetTextWrapSquare(wml.WdST_WrapTextBothSides)
              
              //添加表格
              table :=doc.AddTable()
              // width of the page
              table.Properties().SetWidthPercent(100)
              // with thick borers
              borders :=table.Properties().Borders()
              borders.SetAll(wml.ST_BorderSingle, color.Auto, measurement.Zero)
              row :=table.AddRow()
              row.AddCell().AddParagraph().AddRun().AddText("姓名")
              row=table.AddRow()
              row.AddCell().AddParagraph().AddRun().AddText("hello")
          
              doc.SaveToFile("document.docx") // 保存文件路徑,此處應為絕對路徑
          }
          

          nguyenthenguyen/docx

          讀取docx文件

          示例:

          package main
          import (
              "fmt"
              "github.com/nguyenthenguyen/docx"
          )
          func main() {
              // Read from docx file
              r, err :=docx.ReadDocxFile("./demo.docx")
              // Or read from memory
              // r, err :=docx.ReadDocxFromMemory(data io.ReaderAt, size int64)
          
              // Or read from a filesystem object:
              // r, err :=docx.ReadDocxFromFS(file string, fs fs.FS)
          
              if err !=nil {
                  panic(err)
              }
              docx1 :=r.Editable()
              //獲取內容
              content:=docx1.GetContent()
              fmt.Println(content)
              r.Close()
          }
          

          編輯docx文件

          示例:

          
          package main
          
          import (
              "fmt"
              "github.com/nguyenthenguyen/docx"
          )
          
          func main() {
              r, err :=docx.ReadDocxFile("./demo.docx")
              if err !=nil {
                  panic(err)
              }
              docx1 :=r.Editable()
              //替換內容
              docx1.Replace("舊文字", "新文字", -1)
              docx1.ReplaceLink("http://example.com/", "https://github.com/nguyenthenguyen/docx", 1)
              //替換頁頭信息
              docx1.ReplaceHeader("head", "頁頭")
              //替換頁尾信息
              docx1.ReplaceFooter("第一頁", "new footer")
              //替換圖片
              //docx1.ReplaceImage("word/media/image1.png", "./new.png")
              docx1.WriteToFile("./new_demo.docx")
              r.Close()
          }
          

          links

          https://pkg.go.dev/github.com/nguyenthenguyen/docx#section-readme https://www.cnblogs.com/xingzr/p/17370295.html


          主站蜘蛛池模板: 精品国产亚洲一区二区三区| 亚洲一区二区三区四区视频 | 尤物精品视频一区二区三区 | 亚洲一区免费在线观看| 国产嫖妓一区二区三区无码| 国产主播福利一区二区| 久夜色精品国产一区二区三区| 精品深夜AV无码一区二区老年| 99久久精品国产高清一区二区 | 国产亚洲一区二区手机在线观看| 亚洲一区二区高清| 精品国产区一区二区三区在线观看| 精品日韩亚洲AV无码一区二区三区| 插我一区二区在线观看| 国产一区二区三区免费观在线| 麻豆国产在线不卡一区二区| 久久精品无码一区二区三区免费| 欧美日韩精品一区二区在线观看| 乱子伦一区二区三区| 中文字幕久久久久一区| 久久99久久无码毛片一区二区| 一区二区网站在线观看| 国产无吗一区二区三区在线欢| 无码一区18禁3D| 日本国产一区二区三区在线观看| 亚洲欧美日韩中文字幕一区二区三区 | 一区二区不卡久久精品| 国产成人av一区二区三区不卡| 国产成人av一区二区三区在线| 国产伦精品一区二区三区无广告| 无码一区二区三区亚洲人妻| 日韩精品一区二区三区中文版| 狠狠做深爱婷婷久久综合一区| 99国产精品欧美一区二区三区 | 一区二区三区四区免费视频| 女人18毛片a级毛片一区二区| 国产精品成人免费一区二区 | 无码丰满熟妇浪潮一区二区AV| 欧亚精品一区三区免费| 国产在线一区二区杨幂| 国产精品女同一区二区久久|