<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 語言實(shí)戰(zhàn):命令行程序(2)

          共 15386字,需瀏覽 31分鐘

           ·

          2021-02-28 20:37

          上一期 我們有了一個最簡單的命令行程序。從命令行參數(shù)輸入一系列的編號,程序就會排好序重新輸出。

          接下來讓我們繼續(xù)改進(jìn)程序。


          目錄


          • 準(zhǔn)備知識

            • 標(biāo)志(flag)參數(shù)

          • 改進(jìn)

            • 按數(shù)值排序

            • 從文件輸入輸出

            • 其它改進(jìn)

          • 練習(xí)


          準(zhǔn)備知識

          標(biāo)志(flag)參數(shù)

          我們在之前已經(jīng)了解過命令行參數(shù)。在 gosort 程序中,就是通過命令行參數(shù),輸入需要排序的編號。

          簡單的命令行參數(shù)輸入后變成了字符串切片,只有位置(下標(biāo))的差別,不方便傳遞復(fù)雜的參數(shù)。如果規(guī)定特定次序的參數(shù)表示特定的含義,不好記憶不說,還無法缺省(因?yàn)橐坏┤笔。涡蚓蛠y了)。而像 gosort 程序這樣,普通參數(shù)(又叫位置參數(shù))數(shù)量不確定,把特殊參數(shù)放到最后面也達(dá)不到缺省的效果。

          我們需要 標(biāo)志(flag)參數(shù),通過在短橫線(也就是減號 - )后面加上參數(shù)名,構(gòu)成一個標(biāo)志,使得參數(shù)變得有名字,不再受限于順序。使用效果類似這樣:

          # 假定 gosort 支持標(biāo)志參數(shù) -x

          # 允許缺省,此時 -x 為默認(rèn)值
          # 123 456 789 為三個普通參數(shù),是需要排序的編號,下同
          gosort 123 456 789

          # 如果 -x 是一個布爾參數(shù),那么有 -x 表示 true
          gosort -x 123 456 789

          # 如果 -x 不是布爾型的參數(shù),那么還要指定 -x 的值
          # 可以使用等號
          gosort -x=out.txt 123 456 789
          # 也可以不用等號,緊接著的第一個參數(shù)被認(rèn)為是指定的值
          gosort -x out.txt 123 456 789

          自己實(shí)現(xiàn)標(biāo)志參數(shù)并不難,只需要檢查每一個命令行參數(shù),找出短橫線開頭的,然后根據(jù)類型決定要不要讀下一個參數(shù)作為值;處理的同時,要把標(biāo)志參數(shù)和位置參數(shù)分開,供后續(xù)使用。雖然不難,實(shí)現(xiàn)起來比較瑣碎,一不小心會漏掉一些邊界條件,需要耐心地去測試完善。

          你可以嘗試實(shí)現(xiàn)看看。不過這里我們偷個懶,使用標(biāo)準(zhǔn)庫自帶的 flag 包。

          // 注意 xFlag 不是 bool 而是 *bool(bool指針)
          // 創(chuàng)建時的三個參數(shù),分別是參數(shù)名,默認(rèn)值和參數(shù)說明
          var xFlag = flag.Bool("x", flase, "試用 flag 參數(shù),默認(rèn)為 false")
          // 解析需要放在所有 flag 設(shè)置好之后
          flag.Parse()
          // 需要解引用獲取 bool 值
          if *xFlag {
              // -x 設(shè)置為 true 時的操作
          }
          // 可以通過 flag 包訪問位置參數(shù)的數(shù)量和值(去掉了程序名和標(biāo)志參數(shù))
          fmt.Println("位置參數(shù)一共有", flag.NArg(), "個,分別是:", flag.Args())

          除了直接生成標(biāo)志參數(shù)后返回儲存地址(指針),也可以聲明好變量之后,在設(shè)置標(biāo)志參數(shù)時指定儲存的變量。

          // 注意這里的 name 直接就是 string,不是指針
          var name string
          // 將 name 的地址傳給標(biāo)志參數(shù)
          flag.StringVar(&name, "name""無名""試用 string 類型的 flag 參數(shù)")
          // 解析時參數(shù)會儲存到指定的變量
          flag.Parse()
          // name 本身就是 string,無需解引用
          fmt.Println(name)

          簡單總結(jié)一下:

          • flag 包保存著關(guān)于標(biāo)志參數(shù)的全局狀態(tài), flag.Parse()  必須在 所有標(biāo)志參數(shù)設(shè)置好之后、訪問任意一個參數(shù)值之前調(diào)用
          • flag 只支持基本類型 + time.Duration 類型的參數(shù),每個類型有兩個設(shè)置函數(shù):不帶 Var 結(jié)尾的直接返回儲存變量的指針,帶 Var 結(jié)尾的則需要你指定指針。
          • 在使用時,標(biāo)志參數(shù)必須位于所有位置參數(shù)之前,否則會被當(dāng)做位置參數(shù)。例如 gosort -x 123 456 -name bob 無法得到 name 參數(shù),反而會得到這樣的位置參數(shù):["123","456","-name","bob"]
          • 標(biāo)志參數(shù)名盡量避開 hhelp ,因?yàn)?flag 包默認(rèn)實(shí)現(xiàn)了這兩個標(biāo)志,打印幫助信息。

          更復(fù)雜的標(biāo)志參數(shù),可以使用第三方包 github.com/urfave/cligithub.com/spf13/cobra,它們在 flag 的基礎(chǔ)上,封裝了更高級的用法。但目前為止,flag 已經(jīng)夠用了。

          記得標(biāo)準(zhǔn)庫和第三方包的詳細(xì)文檔,可以在 pkg.go.dev 搜到。

          在這一期里,部分函數(shù)只會做簡略的介紹,詳細(xì)的函數(shù)簽名和用法需要大家自行看文檔。

          改進(jìn)

          篇幅關(guān)系,只展示代碼的關(guān)鍵部分,需要補(bǔ)足剩余的代碼才能編譯運(yùn)行。

          在開始改進(jìn)之前,先將之前的程序做一點(diǎn)小調(diào)整:把排序和拼接的代碼,單獨(dú)抽取成一個函數(shù):

          func main() {
              // 為了不修改包級變量(os.Args 以及后面的 flag.Args())
              // 排序前先拷貝一份
              s := make([]stringlen(os.Args)-1)
              copy(s, os.Args[1:]))
              fmt.Println(sortStrings(s))
          }

          // 因?yàn)椴恍枰┌庹{(diào)用,函數(shù)名小寫開頭即可
          // 上一期介紹過函數(shù)簽名和函數(shù)原型,讀者想必可以看懂這個函數(shù)的參數(shù)和返回值
          func sortStrings(strs []string) string {
              sort.Strings(strs)
              return strings.Join(strs, ",")
          }

          將功能相對獨(dú)立的、會被復(fù)用的代碼抽取成函數(shù)是一個好的編程習(xí)慣,將函數(shù)內(nèi)部控制在較少的容易理解的行數(shù),可以讓程序代碼行數(shù)持續(xù)膨脹的同時,保持一個較好的可讀性。

          按數(shù)值排序

          還記得我們的需求嗎?待排序的是一組提交編號,它們是單調(diào)遞增的序列號。在實(shí)際使用中,因?yàn)榇a庫特別龐大,到后期提交編號達(dá)到好幾位數(shù),位數(shù)要過很長時間才增長一位,給人一種編號位數(shù)一直就是這么多的錯覺。實(shí)際上并不是這樣,編號用完了還是要進(jìn)位的,9999 之后,就是 10000 了。

          之前的實(shí)現(xiàn)按文本(字符串)排序,就出問題了。例如 9,80,564,1253 這幾個數(shù),如果按照數(shù)值排序,現(xiàn)在的順序就是升序;可如果按字符串排序,則剛好反過來,順序是 1253,564,80,9 。因?yàn)樽址穷^部對齊后從左到右比較的,前綴分出先后就直接結(jié)束比較。

          我們需要先將提交編號轉(zhuǎn)換為數(shù)字,再按數(shù)字的規(guī)則排序。另一方面,按照字符串排序可以保留,讓 gosort 程序有更多的用途,這時就需要一個標(biāo)志參數(shù)區(qū)分開。現(xiàn)在規(guī)定默認(rèn)情況按數(shù)值排序,而當(dāng)設(shè)置 -l (lexically)時按字符串排序。

          func main() {
              // 設(shè)置 bool 型的標(biāo)志參數(shù) -l
              var lex bool
              flag.BoolVar(&lex, "l"false"sort lexically")
              flag.Parse()

              var res string
              if lex {
                  // 如果設(shè)置了 -l,調(diào)用之前的sortStrings()
                  // flag.Args() 返回的切片在 Parse() 的時候已經(jīng)去掉了多余的參數(shù)
                  s := make([]string, flag.NArg())
                  copy(s, flag.Args())
                  res = sortStrings(s)
              } else {
                  // 否則先轉(zhuǎn)為整型切片再排序
                  // 由于沒有對返回的切片進(jìn)行修改,所以無需拷貝
                  nums, err := strsToNums(flag.Args())
                  // 命令行參數(shù)不一定都是數(shù)字,轉(zhuǎn)換有可能失敗,此時要打印錯誤信息并退出
                  if err != nil {
                      fmt.Println(err)
                      // 0 以外的值表示異常退出
                      // 退出后,后面的代碼都不會再執(zhí)行
                      os.Exit(1)
                  }
                  res = sortNums(nums)
              }
              fmt.Println(res)
          }

          首先是設(shè)置標(biāo)志參數(shù)并儲存在變量 lex ,這部分內(nèi)容參考前面的準(zhǔn)備知識。然后程序根據(jù) lex 的值執(zhí)行不同的分支。如果是字符串排序,就是之前的函數(shù)。另外一個分支則多了幾個新函數(shù)。

          strsToNums() 是我們自己實(shí)現(xiàn)的函數(shù),用來把字符串切片轉(zhuǎn)換為整型數(shù)切片。因?yàn)檗D(zhuǎn)換有可能失敗,所以返回值列表里還帶著一個 error 類型的返回值。

          func strsToNums(strs []string) ([]int, error) {
              // 創(chuàng)建同樣大小的 int 切片,用來存放轉(zhuǎn)換的結(jié)果
              nums := make([]intlen(strs))
              var err error
              // 遍歷每個字符串并轉(zhuǎn)換
              for i := range strs {
                  nums[i], err = strconv.Atoi(strs[i])
                  // 這里的邏輯是,只要其中一個字符串轉(zhuǎn)換失敗,就返回空白切片和錯誤
                  // 你也可以改為忽略不能轉(zhuǎn)換的字符串,繼續(xù)轉(zhuǎn)換和排序
                  if err != nil {
                      return nil, err
                  }
              }
              return nums, nil
          }

          這里使用了標(biāo)準(zhǔn)庫函數(shù) strconv.Atoi()strconv 是 String  Convert 的縮寫,這個包里是跟字符串轉(zhuǎn)換相關(guān)的工具函數(shù),其中 Atoi() 就是把按十進(jìn)制顯示的字符串,轉(zhuǎn)換為 int 型。因?yàn)樽址畠Υ娴牟灰欢ㄊ鞘M(jìn)制數(shù),就有可能轉(zhuǎn)換失敗。

          sortNums() 是另一個我們自己實(shí)現(xiàn)的函數(shù),跟之前的 sortStrings() 類似。

          func sortNums(nums []int) string {
              sort.Ints(nums)
              return numsJoin(nums)
          }

          由于整型切片無法直接 strings.Join() ,為了讓 sortNums() 內(nèi)部跟 sortStrings() 保持類似,我們又自行實(shí)現(xiàn)了 numsJoin()。把整型數(shù)拼接成逗號隔開的字符串有很多種具體的做法,這里是其中一種:

          // 實(shí)現(xiàn) 1
          func numsJoin(nums []int) string {
              // 一種直觀的想法就是,再逐個轉(zhuǎn)換回字符串,再 strings.Join()
              strs := make([]stringlen(nums))
              for i := range nums {
                  // 每個整型數(shù)都一定有對應(yīng)的字符串表示,不存在失敗,所以返回值里沒有 error
                  strs[i] = strconv.Itoa(nums[i])
              }
              return strings.Join(strs, ",")
          }

          不過我嫌這里做了兩次轉(zhuǎn)換(先從整型到一個個字符串,再把字符串拼成長字符串),有點(diǎn)浪費(fèi)。能不能一步到位呢,于是我又寫了第二種實(shí)現(xiàn):

          // 實(shí)現(xiàn) 2
          func numsJoin(nums []int) string {
              // 聲明一個 byte 切片作為緩沖區(qū)
              // 這里無需 make,因?yàn)楹罄m(xù)的兩個 append 操作都會根據(jù)需要擴(kuò)展切片,包括 nil 切片也能處理
              var buf []byte
              for _, n := range nums {
                  // 把轉(zhuǎn)換后的字符串放進(jìn)緩沖區(qū)(以字節(jié)的形式)
                  strconv.AppendInt(buf, int64(n), 10)
                  // 把逗號放進(jìn)緩沖區(qū)
                  append(buf, ',')
              }
              // byte 切片轉(zhuǎn)換為字符串,只轉(zhuǎn)換一次
              return string(buf)
          }

          我還能基于 bytes.Bufferstrings.Builder 寫出別的實(shí)現(xiàn)版本。

          但第一種實(shí)現(xiàn)就挺好的。這里只是展示,有時同一個功能可以有多種實(shí)現(xiàn)方式。開發(fā)的首要任務(wù)是實(shí)現(xiàn)功能,并且盡可能讓代碼易讀,不容易出錯。性能有時也重要,但必須是經(jīng)過分析,確認(rèn)有性能差異,并且這個差異對于程序的表現(xiàn)有影響。第二種實(shí)現(xiàn)一定比第一種性能好嗎?差異是否大到值得特意去優(yōu)化?使用看起來性能好但是不熟悉的實(shí)現(xiàn),是否會帶入潛在的 bug?答案都是不確定的。

          這里為了行文方便,每個函數(shù)分開討論,實(shí)際上它們都放在 main.go 里。在 Go 里,包級成員(包括函數(shù))的引用順序和聲明順序無關(guān),只要不存在循環(huán)引用即可。一般的慣例是,init()main (如果有)最前面,然后是導(dǎo)出(exported)成員(就是首字母大寫那些),然后是未導(dǎo)出(unexported)成員。未導(dǎo)出函數(shù)之間,先被引用到的就放前面。

          現(xiàn)在重新編譯之后執(zhí)行一下程序看看效果:

          > gosort -h
          Usage of gosort:
            -l    sort lexically

          > gosort 1253 80 9 564
          9,80,564,1253

          > gosort 1253 abc 80 9 564
          strconv.Atoi: parsing "abc": invalid syntax

          > gosort -l 1253 abc 80 9 564
          1253,564,80,9,abc

          從文件輸入輸出

          現(xiàn)在程序默認(rèn)按數(shù)值排序,即使遇到長度不同的提交編號也不怕;同時按文本排序的功能也沒丟掉,偶爾還能用來排一下人名之類的文本信息。程序夠用了嗎?

          還是回到最初的需求。為什么不人工檢查排序呢?因?yàn)榫幪柖啵疃噙_(dá)幾十上百,這種數(shù)量,人工排序又慢又累又容易錯。甚至不要說排序,就是把編號全部輸入一遍,也是慢、累、易錯。實(shí)際工作中都是打開記事本,把大家回復(fù)的編號整理起來,然后直接復(fù)制到命令行作為參數(shù)。有時還得追加提交編號,就把新的編號放到記事本最后面,然后 Ctrl + A(全選),Ctrl + C(復(fù)制),來到命令行,Ctrl + V(粘貼),熟練到麻木。

          為什么要重復(fù)做這四個動作,能不能告訴程序,直接從指定的文件讀編號?當(dāng)然可以。不過從命令行參數(shù)輸入編號還是得保留,方便數(shù)量少時使用。

          這時可以設(shè)置標(biāo)志參數(shù) -f (file,不過為了跟后面的輸出區(qū)分,還是理解為 from 吧),傳入一個文件,讓程序改為從文件讀入。這里假定文本文件里只有編號,以空白字符隔開。

          func main() {
              // 省略 -l 部分代碼...
              var from string
              flag.StringVar(&from, "-f""""input from file")
              flag.Parse()
              
              var strs []string
              if from != "" {
                  // 如果不是文件,輸出錯誤信息并退出
                  if !isFile(from) {
                      fmt.Println(from, " is not a file")
                      os.Exit(1)
                  }

                  // 讀取文件有多種實(shí)現(xiàn)方式,這里采用了最簡單的 ioutil 包
                  buf, err := ioutil.ReadFile(from)
                  // 如果讀取文件有錯誤,也是輸出錯誤信息并退出
                  if err != nil {
                      fmt.Println("read ", from, " fail: ", err)
                      os.Exit(1)
                  }

                  // 這里不要用 strings.Spilt(string(buf), " "),因?yàn)殚g隔的空白字符可能不止一個
                  // Fields() 以任意數(shù)量的空白字符作為分割
                  strs = strings.Fields(string(buf))
              }
              // 無論是否從文件讀入,位置參數(shù)都追加到后面
              // ... 的含義,請參考上一期可變參數(shù)部分
              strs = append(strs, flag.Args()...)

              var res string
              if lex {
                  // 前面 append 時字符串追加到了新切片,這里不用再拷貝
                  res = sortStrings(strs)
              } else {
                  nums, err := strsToNums(strs)
              // 省略之后的代碼...
          }

          增加了 -f 參數(shù)之后,如果 from 有值,而且這個值確實(shí)是一個有效的文件,就會從里面讀取內(nèi)容。位置參數(shù)的值同時也追加到切片里。

          這里面用到的新函數(shù),只有 isFile() 是自行實(shí)現(xiàn),其它像 ioutil.ReadFile()strings.Fields() 可以直接查詢文檔。

          // 判斷是否文件的固定套路
          func isFile(path string) bool {
              info, err := os.Stat(path)
              if err != nil && !os.IsExist(err) {
                  return false
              }

              return !info.IsDir()
          }

          相應(yīng)地,從命令行復(fù)制大段的輸出也不夠方便。極端的情況下,太多的輸出甚至?xí)雒钚械木彌_區(qū)。可以從文件輸入,自然也可以從文件輸出。跟 -f 參數(shù)類似,我使用 -o (output)參數(shù)指定輸出文件。參數(shù)設(shè)置就不再示范了,這里看一下怎么寫入文件。

          func main() {
              // 省略...
              var output string
              
              // 省略...
              
              if output == "" {
                  fmt.Println(res)
              } else {
                  err := ioutil.WriteFile(output, []byte(res), 0666)
                  if err != nil {
                      fmt.Println("write result to ", output, " fail: ", err)
                      os.Exit(1)
                  }
              }
          }

          同樣地,為了偷懶,直接用 ioutil.WriteFile() ,三個參數(shù)分別是文件名(路徑),寫入的數(shù)據(jù)(string 需要轉(zhuǎn)換為 字節(jié)切片)和 文件權(quán)限。這個函數(shù)在遇到目標(biāo)文件存在且有寫權(quán)限時,會直接覆蓋原來的內(nèi)容,但不改動權(quán)限;如果文件不存在,則以指定權(quán)限創(chuàng)建文件。0666 為八進(jìn)制數(shù),對應(yīng) Linux 的權(quán)限值(在 Windows 系統(tǒng) Go 會自動轉(zhuǎn)換為相應(yīng)的操作)。

          接下來試一下修改后的程序。我們首先創(chuàng)建一個 input.txt (注意前后和中間間隔有多余空格,實(shí)際操作中不小心多輸入空格是非常常見的):

            123 456 111      983    

          然后執(zhí)行程序:

          # 使用 strings.Spilt(string(buf), " ") 分割文件輸入的效果
          # 分割結(jié)果里出現(xiàn)了空白字符串,無法轉(zhuǎn)換為數(shù)字
          > gosort -f input.txt 256
          strconv.Atoi: parsing "": invalid syntax

          # 使用 strings.Fields(string(buf)) 的效果
          > gosort -f input.txt 256
          111,123,256,456,983

          # 參數(shù)較多時,為了視覺上緊湊,可以用等號連接
          > gosort -f=input.txt -o=output.txt 256
          # 沒有錯誤的話沒有輸出,結(jié)果在 output.txt 里

          其它改進(jìn)

          除此之外,還能想到一些有用的功能。

          去重

          無論是有人不小心回復(fù)了重復(fù)的編號,還是管理員整理時多寫了,編號偶爾會出現(xiàn)重復(fù)。重復(fù)的內(nèi)容,在不同場景下,造成的影響可大可小。對于一些嚴(yán)格的場景,我們更希望編號里沒有重復(fù)。這就要求對結(jié)果進(jìn)行去重。

          這時我們可以增加一個 -u (unique)的 bool 參數(shù),表示開啟去重。(當(dāng)然也可以默認(rèn)去重,設(shè)置一個反向的開關(guān)表示保留重復(fù)。)去重可以在排序之后做,因?yàn)檫@時重復(fù)元素相鄰,更容易處理。這個功能實(shí)現(xiàn)起來并不難,對切片做一次遍歷即可,大家可以嘗試自己實(shí)現(xiàn)。如果一時沒有概念,可以先用 Go 語言完成這道題 https://leetcode-cn.com/problems/remove-element/ ,做出這道題就知道如何高效地在切片里去除元素了。

          需要注意的是,因?yàn)橛?[]string[]int 兩種切片,去重的邏輯可能要實(shí)現(xiàn)兩個版本(視乎你的代碼實(shí)現(xiàn))。這是 Go 目前不便的其中一個地方:沒有泛型,會一定程度導(dǎo)致代碼重復(fù)。

          自定義分隔符

          現(xiàn)在的程序,根據(jù)常用的場景,默認(rèn)了輸入的分隔符是空白字符,輸出的分隔符是半角逗號 , 。但這個設(shè)定不會總是好使。有可能輸入文件是其他人整理的,可能是別的系統(tǒng)導(dǎo)出的,用了別的分隔符;輸出內(nèi)容也可能用于別的場景,需要別的格式。

          這時我們需要針對每個場景修改代碼,重新編譯嗎?沒有必要。只要設(shè)置對應(yīng)的標(biāo)志參數(shù),允許分別指定分隔符就好。而如果沒有指定,還是用原本的默認(rèn)符號。兩個符號分別用于分割輸入的內(nèi)容,和拼接輸出的內(nèi)容,需要調(diào)整相關(guān)的代碼,可能用到的函數(shù)基本都在 strings 包里。

          這兩個功能添加上之后,實(shí)現(xiàn)效果大概是這樣的:

          # 這里我定義輸入分隔符的參數(shù)為 -i,輸出分隔符的參數(shù)為 -s
          > gosort -u -i=, -s=- 111,111,555,678,333,567,678
          111-333-555-567-678

          練習(xí)

          1. numsJoin() 的第二種實(shí)現(xiàn)有幾個 bug,你發(fā)現(xiàn)了嗎?
          2. 通過自行查閱文檔,了解 strings.Builder 的用法,你可以寫出 numsJoin() 的第三種實(shí)現(xiàn)嗎?
          3. 你可以自行實(shí)現(xiàn) 去重自定義分隔符 兩個功能嗎?


          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 47
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人精品小电影 | 日韩一区二区三区操b | 欧美日韩在线电影 | 欧美性爱在线 | 亚洲五月天综合 |