json操作
JSON (JavaScript Object Notation)是一種比XML更輕量級的數據交換格式,不僅易於閱讀和理解,也更方面程序解析和生成。盡管json是JavaScript的一個子集,但是json采用完全獨立於編程語言的文本模式,而且表現形式類似於鍵值對(如果你了解Python的話,那么你會發現json的字典很相似,其實json就是借鑒了Python中字典的結構),這使它成為了理想的、跨平台、跨語言的數據交換語言。
{
"name": "夏色祭",
"age": 16,
"gender": "female",
"中の人": {
"name": "佐藤希",
"age": 25,
"gender": "female"
}
}
開發者可以用 JSON 傳輸簡單的字符串、數字、布爾值,也可以傳輸一個數組,或者一個更復雜 的復合結構。在 Web 開發領域中, JSON被廣泛應用於 Web 服務端程序和客戶端之間的數據通信。
Go語言內建對JSON的支持,我們可以使用內置的encoding/json包標准庫,輕松地對json數據進行解析和生成。
下面就來看看吧。
結構體和json
生成json可以調用json.Marshal函數,或者json.MarshalIndent,兩個函數是一樣的,只不過后者會通過縮進使格式更美觀一些。
func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
舉個栗子:
package main
import (
"encoding/json"
"fmt"
)
type girl struct {
Name string
Age int
Hobby string
}
func main() {
g := girl{"夏色祭", 16, "まつり"}
res, err := json.Marshal(g)
if err != nil {
fmt.Println("err =", err)
}
fmt.Println(string(res))
//{"Name":"夏色祭","Age":16,"Hobby":"まつり"}
res, err = json.MarshalIndent(g, "", "\t")
if err != nil {
fmt.Println("err =", err)
}
fmt.Println(string(res))
/*
{
"Name": "夏色祭",
"Age": 16,
"Hobby": "まつり"
}
*/
}
但是有一點需要注意,結構的成員必須要大寫,否則無法被導出成json。但如果我們就希望小寫呢,答案是使用tag。
package main
import (
"encoding/json"
"fmt"
)
type girl struct {
Name string `json:"name"`
Age int `json:"age"`
Hobby string `json:"hobby"`
}
func main() {
g := girl{"夏色祭", 16, "まつり"}
res, err := json.Marshal(g)
if err != nil {
fmt.Println("err =", err)
}
fmt.Println(string(res))
//{"name":"夏色祭","age":16,"hobby":"まつり"}
}
除了根據結構體得到json,我們也可以將一個json解析成結構體。
package main
import (
"encoding/json"
"fmt"
)
type girl struct {
//使用`json:"xxx"`相當於起了一個別名xxx, 以后序列化出來的字段就叫這個名字
Name string `json:"name"`
Age int `json:"age"`
Hobby string `json:"hobby"`
}
func main() {
var g girl
s := `
{
"name": "神樂mea",
"age": 38,
"hobby": "money"
}
`
// 解析到結構體中的話, 可以使用json.Unmarshal
// 傳入字符數組和結構體指針
err := json.Unmarshal([]byte(s), &g)
if err != nil {
fmt.Println("err =", err)
}
fmt.Println(g) // {神樂mea 38 money}
}
map和json
除了結構體和json可以互轉之外, map也是可以的,當然調用的函數都是一樣的,我們來看一下。
package main
import (
"encoding/json"
"fmt"
)
func main() {
// 創建一個保存鍵值對的映射
t1 := make(map[string]interface{})
t1["company"] = "博客園"
t1["subjects"] = []string{"Go", "Web", "Python", "C++"}
t1["isok"] = true
t1["price"] = 8980.88
res, err := json.MarshalIndent(t1, "", "\t")
if err != nil {
fmt.Println("err =", err)
}
fmt.Println(string(res))
/*
{
"company": "博客園",
"isok": true,
"price": 8980.88,
"subjects": [
"Go",
"Web",
"Python",
"C++"
]
}
*/
}
將json轉成map也是可以的,使用的也是同一個函數:
package main
import (
"encoding/json"
"fmt"
)
func main() {
s := `
{
"company": "博客園",
"isok": true,
"price": 8980.88,
"subjects": [
"Go",
"Web",
"Python",
"C++"
]
}
`
//默認返回值類型為interface類型, 我們以map類型進行格式存儲
//可以理解為: json的key為map的key, json的value為map的value
//格式: map[string]interface{}
var t interface{}
err := json.Unmarshal([]byte(s), &t)
if err != nil {
fmt.Println("err =", err)
}
//因為map是無序的, 所以打印的結果不一定和我們想象的一樣
fmt.Println(t) // map[company:博客園 isok:true price:8980.88 subjects:[Go Web Python C++]]
// 使用斷言判斷類型
m := t.(map[string]interface{})
for k, v := range m {
switch val := v.(type) {
case string:
fmt.Println(k, "is string", val)
case int:
fmt.Println(k, "is int", val)
case float64:
fmt.Println(k, "is float64", val)
case bool:
fmt.Println(k, "is bool", val)
case []interface{}:
fmt.Println(k, "is array")
for i, u := range val {
fmt.Println(i, u)
}
default:
fmt.Println("Unknown type")
}
}
/*
company is string 博客園
isok is bool true
price is float64 8980.88
subjects is array
0 Go
1 Web
2 Python
3 C++
*/
}
將數據結構轉成json這一步稱之為序列化,根據json得到相應數據結構這一步稱之為反序列化。
而除了json之外,在Go中還有兩種序列化和反序列化的方式,我們來看一下。
gob
標准庫gob是Go語言提供的 "私有" 的編解碼方式,它的效率會比json,xml等更高,特別適合在Go語言程序間傳遞數據。
它的序列化和反序列化同樣很簡單:
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type Girl struct {
Name string
Age int `json:"age"`
Gender string `json:"gender"`
Where string `json:"where"`
Is_married bool `json:"is_married"`
}
func main() {
g := Girl{"satori", 16, "f", "東方地靈殿", false}
//創建緩存
buf := new(bytes.Buffer)
//把指針丟進去
enc := gob.NewEncoder(buf)
//調用Encode進行序列化
if err := enc.Encode(g); err != nil {
fmt.Println(err)
return
} else {
//序列化的內容會被放進buf里面
fmt.Println(buf.String())
/*
G��Girl�� Name Age Gender Where
Is_married !��satori f東方地靈殿
*/
}
}
發現是亂碼,因為這類似Python的pickle,是該語言獨有的。所以我們不認識沒關系,Go編譯器認識就行了
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type Girl struct {
Name string
Age int `json:"age"`
Gender string `json:"gender"`
Where string `json:"where"`
Is_married bool `json:"is_married"`
}
func main() {
g := Girl{"satori", 16, "f", "東方地靈殿", false}
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
if err := enc.Encode(g);err != nil {
fmt.Println(err)
return
}
var g1 = Girl{}
//bytes.NewBuffer和bytes.Buffer類似,只不過可以傳入一個初始的byte數組,返回一個指針
dec := gob.NewDecoder(bytes.NewBuffer(buf.Bytes()))
//調用Decode方法,傳入結構體對象指針,會自動將buf.Bytes()里面的內容轉換成結構體
if err := dec.Decode(&g1);err != nil {
fmt.Println(err)
return
} else {
fmt.Println(g1) // {satori 16 f 東方地靈殿 false}
}
}
msgpack
MessagePack是一種高效的二進制序列化格式。它允許你在多種語言(如JSON)之間交換數據。但它更快更小。
這個包屬於第三方,我們使用的話需要先安裝:go get -u github.com/vmihailenco/msgpack
,而且這個包的接口和標准庫json是一致的。
package main
import (
"fmt"
"github.com/vmihailenco/msgpack"
)
type Girl struct {
Name string
Age int `json:"age"`
Gender string `json:"gender"`
Where string `json:"where"`
Is_married bool `json:"is_married"`
}
func main() {
g := Girl{"satori", 16, "f", "東方地靈殿", false}
//這個沒有MarshalIndent
if ret, err := msgpack.Marshal(g); err != nil {
fmt.Println(err)
return
} else {
fmt.Println(string(ret)) //��Name�satori�Age� �Gender�f�Where�東方地靈殿�Is_married�
var g1 = Girl{}
if err := msgpack.Unmarshal(ret, &g1); err != nil {
fmt.Println(err)
return
} else {
fmt.Println(g1) // {satori 16 f 東方地靈殿 false}
}
}
}
文件操作
下面說一下Go語言中的文件操作,操作文件想必再熟悉不過了,Go語言中處理文件有一個特點,就是不用指定編碼,因為默認是utf-8編碼。
那么我們來看看在Go里面如何操作文件吧。
創建文件並寫入內容、打開文件並讀取內容
首先是創建文件並寫入內容:
首先是創建文件,創建文件使用os.Create
函數:
package main
import (
"fmt"
"os"
)
func main() {
// 根據提供的文件名創建一個新的文件, 返回一個文件句柄 和 一個錯誤信息
// 如果創建失敗, 那么file為nil, err為錯誤信息; 創建成功file為對應的文件句柄, err為nil
file, err := os.Create("1.txt")
if err != nil {
panic(fmt.Sprintf("文件創建失敗, 失敗原因: %s\n", err.Error()))
}
// 寫入內容, 可以調用WriteString寫入字符串, 也可以調用write寫入字節數組
_, _ = file.Write([]byte("古明地覺"))
_, _ = file.WriteString("你好呀")
// 這兩個方法都會返回一個 int 和 一個error, 分別表示寫入的字節數和錯誤信息
// 寫入成功: 返回 寫入字節數 和 nil; 寫入失敗: 返回 0 和 錯誤信息
// 最后記得關閉文件, 當然最好的辦法是在創建文件之后, 就是用defer語句
file.Close()
}
該文件寫入時的權限默認是0666,並且使用os.Create時如果文件已經存在,那么會先將文件清空。
然后是讀取文件,讀取文件使用的是os.Open
函數:
package main
import (
"fmt"
"os"
)
func main() {
// 指定文件名, 默認以utf-8編碼模式打開文件, 同樣會返回一個文件句柄和錯誤信息
// 如果文件不存在: 那么就會報錯, error不為nil
file, err := os.Open("1.txt")
if err != nil {
panic(fmt.Sprintf("文件打開失敗, 錯誤信息:%s\n", err.Error()))
}
defer file.Close()
// 下面來讀取文件, 讀取文件使用Read方法即可
// 我們需要指定一個切片, 會將內容都讀到切片中, 一般指定長度為1024
buf := make([]byte, 1024)
//此時文件的內容都會讀到buf里面, n則是寫入了多少個字節, n不會超過切片buf的長度
n, err := file.Read(buf)
// 將寫入的內容讀取出來
fmt.Println(string(buf[: n])) // 你好呀古明地覺
}
我們注意到:當前指定的buf的長度為1024,那如果文件的字節數超過1024該怎么辦呢?於是我們可以將長度變得更大一些,但是到底要弄多大呢?這是一個未知數。弄小了一次讀不完,要是弄大了,會浪費。因此最好的辦法,不要一次就讀完,而是循環讀取,這樣不就好了嗎?
這里我手動往1.txt文件里面寫入一些內容吧。
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 指定文件名, 默認以utf-8編碼模式打開文件, 同樣會返回一個文件句柄和錯誤信息
// 如果文件不存在: 那么就會報錯, error不為nil
file, err := os.Open("1.txt")
if err != nil {
panic(fmt.Sprintf("文件打開失敗, 錯誤信息:%s\n", err.Error()))
}
defer file.Close()
buf := make([]byte, 12) // 存放文件內容的緩存,相當於中轉站
data := make([]byte, 0, 1024) // 用來存放文件內容,buf讀取的內容都會寫到data里面去
for {
//無限循環,不斷讀取
n, err := file.Read(buf)
// 什么時候文件讀完呢?如果文件讀完的話,那么err不為nil,而是io.EOF
// 所以我們可以進行判斷
if err != nil {
//如果err != nil說明出錯了,但如果還等於io.EOF的話,說明讀完了,因為文件讀完,err也不為nil。直接break
if err == io.EOF {
break
} else {
//如果錯誤不是io.EOF的話,說明就真的在讀取中出現了錯誤,直接panic出來
panic(err)
}
}
//此時文件內容寫到buf里面去了,寫了多少個呢?寫了n個,那么我們再寫到data里面去
data = append(data, buf[:n]...)
//我們來打印一下,每次寫了多少個字節
fmt.Printf("往data中寫入%d個字節\n", n)
/*
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入12個字節
往data中寫入6個字節
*/
}
//寫完之后,我們來打印一下
fmt.Println(string(data))
/*
白色相簿什么的,已經無所謂了。
因為已經不再有歌,值得去唱了。
傳達不了的戀情,已經不需要了。
因為已經不再有人,值得去愛了。
*/
}
以上便是文件的基本操作,注意:在讀取的時候我們指定了buf和data兩個切片,buf切片用於接收文件的內容,接收完畢之后再寫入到data里面去。並且為了演示,我們的buf的長度指定為12,比較少,一般指定為1024。
無論寫入還是讀出,我們從前往后,但是我們還可以在指定位置寫入、在指定位置讀取,舉個栗子:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("1.txt")
if err != nil {
panic(fmt.Sprintf("文件創建失敗, 失敗原因: %s\n", err.Error()))
}
_, _ = file.Write([]byte("古明地覺"))
_, _ = file.WriteString("你好呀")
// 我們可以調用ReadAt在指定位置寫入, 在偏移量為9的位置寫入 "戀"
// 依舊返回一個 int 和 error, 表示寫入的字節數 以及 錯誤信息
_, _ = file.ReadAt([]byte("戀"), 9)
}
然后我們讀取一下看看:
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("1.txt")
if err != nil {
panic(fmt.Sprintf("文件打開失敗, 錯誤信息:%s\n", err.Error()))
}
defer file.Close()
buf := make([]byte, 1024)
data := make([]byte, 0, 1024)
for {
n, err := file.Read(buf)
if err != nil {
if err == io.EOF {
break
} else {
panic(err)
}
}
data = append(data, buf[:n]...)
}
fmt.Println(string(data)) // 古明地戀你好呀
}
我們在讀取之后發現"覺"被替換成了"戀","古明地"總共是9字節,因此在偏移量為9的位置寫入正好把"覺"這個字符給替換掉了。從這里我們也能看出,這里在中間位置寫入的時候會將后面的內容覆蓋掉。當然讀取的話也有一個ReadAt,可以自己試一下。但無論是WriteAt還是ReadAt我們用的都比較少,更推薦的方式是移動光標,后面會說。
os.OpenFile
其實無論是os.Create還是os.Open,它們底層都調用了os.OpenFile函數,我們來看一下。
package main
import (
"fmt"
"os"
)
func main() {
// OpenFile接收三個參數
// 1.文件名
// 2.文件的模式, 支持如下
/*
os.O_RDONLY: 以只讀的方式打開
os.O_WRONLY: 以只寫的方式打開
os.O_RDWR: 以讀寫的方式打開
os.O_NONBLOCK: 打開時不阻塞
os.O_APPEND: 以追加的方式打開
os.O_CREAT: 創建並打開一個新文件
os.O_TRUNC: 打開一個文件並截斷它的長度為零(必須有寫權限)
os.O_EXCL: 如果指定的文件存在,返回錯誤
os.O_SHLOCK: 自動獲取共享鎖
os.O_EXLOCK: 自動獲取獨立鎖
os.O_DIRECT: 消除或減少緩存效果
os.O_FSYNC : 同步寫入
os.O_NOFOLLOW: 不追蹤軟鏈接
*/
// 3.權限,一般設置為0666,這在linux下有用,Windows下面沒太大卵用
// 以只寫的方式打開,並且寫入的時候,是以追加的形式寫入
file, _ := os.OpenFile(`1.txt`, os.O_WRONLY|os.O_APPEND, 0666)
_, _ = file.Write([]byte("\n這是新的內容,但是原來的內容還在"))
// 關閉文件
defer file.Close()
//寫入之后,我們再以只讀方式打開
file, _ = os.OpenFile(`1.txt`, os.O_RDONLY, 0666)
buf := make([]byte, 1024)
n, _ := file.Read(buf)
fmt.Println(string(buf[:n]))
/*
古明地戀你好呀
這是新的內容,但是原來的內容還在
*/
}
下面說一說光標,首先我們上面寫入內容之后關閉文件,然后再打開文件是不是比較麻煩呢,可不可以用讀寫模式打開呢?
package main
import (
"fmt"
"os"
)
func main() {
// OpenFile接收三個參數
file, err := os.OpenFile(`1.txt`, os.O_RDWR|os.O_TRUNC, 0666)
if err != nil {
panic(err)
}
_, _ = file.Write([]byte("你好呀, 古明地覺"))
buf := make([]byte, 1024)
n, _ := file.Read(buf)
fmt.Printf("%q\n", string(buf[: n])) // ""
}
我們發現明明寫入了內容,但是卻什么也沒有讀到,這里便涉及到文件的光標了。光標就是當前所處的位置,默認在讀取的時候只能往后讀;而上面在寫完內容之后,光標就直接停在結尾處了,所以此時當然什么也讀不到啦。所以解決辦法就是將光標進行移動即可,而移動光標可以通過file.Seek函數:
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
/*
offset表示向后移動多少個字節
whence表示位置: 0表示文件的開頭、1表示當前光標的所在位置、2表示文件的結尾
*/
// 返回光標移動后所處的位置, 以及錯誤信息
f.Seek(0, 0): 從文件的開始位置向后移動0字節, 相當於將光標調整到文件開始位置;
f.Seek(9, 0): 將光標移動到文件偏移量為9的位置;
f.Seek(3, 1): 將光標從當前位置向后移動3字節
f.Seek(-3, 2): 將光標從文件結尾位置向前移動3字節
下面來演示一下:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.OpenFile(`1.txt`, os.O_RDWR|os.O_TRUNC, 0666)
if err != nil {
panic(err)
}
_, _ = file.Write([]byte("你好呀古明地覺"))
// 我們從結尾處向前移動3字節, 所以移動后光標所處的位置就是 6 * 3 = 18 的位置
pos, _ := file.Seek(-3, 2)
fmt.Println(pos) // 18
buf := make([]byte, 1024)
n, _ := file.Read(buf)
fmt.Printf("%q\n", string(buf[: n])) // "覺"
// 移動到開始的位置
_, _ = file.Seek(0, 0)
n, _ = file.Read(buf)
fmt.Printf("%q\n", string(buf[: n])) // "你好呀古明地覺"
}
以上便是光標,對啦,如果想知道光標某一個時刻的所處位置該怎么辦呢?很簡單,f.Seek(0, 1)
,從當前位置向后移動0個字節不就是當前所處的位置了嗎?
然后再來說說os.OpenFile的第二個參數,也就是模式:
1. 在文件不存在的時候, 必須指定os.O_CREATE; 如果文件存在的話可以不用指定;
2. os.O_TRUNC表示打開的時候清空文件, os.O_APPEND表示打開文件的時候不清空並自動在結尾處追加, 所以這兩個參數不可以同時指定;
3. 指定os.O_CREATE會在文件不存在時創建; 如果文件存在並且在不指定os.O_APPEND的時候, 光標會自動處於文件的開始位置; 如果讀取的話可以將內容全部讀取出來, 寫入的話會從開始位置寫入、因此會將后面的內容覆蓋掉;如果指定了os.O_APPEND, 那么光標會處於文件的結尾, 寫入的時候會向后追加, 但是讀取的時候只會得到空字符串, 因為后面沒有內容了, 想要讀取內容的話需要手動移動光標;
4. os.O_RDONLY表示只讀;O_WRONLY表示只寫;os.O_RDWR表示可讀可寫, 但如果文件不存在則它們都需要搭配os.O_CREATE使用;
5. 打開的文件默認是可讀可寫的, 一般不需要指定os.O_RDWR; 所以寫入文件的話, 指定O_CREATE|O_APPEND或者O_CREATE|O_TRUNC即可, 表示追加寫入或者清空寫入; 讀取文件直接指定O_CREATE, 當然這種情況也是可寫的, 但是我們一般不這么做; 同時讀寫文件的話, 指定O_CREATE|O_APPEND即可, 因為一開始文件可能有內容所以不使用O_TRUNC; 當然啦, 通過光標我們可以更加靈活的進行控制
6. 如果就希望只讀或者只寫的話, 可以使用O_RDONLY或者O_WRONLY
文件屬性
文件是具有很多屬性的,比如:大小、創建時間、類型等等,那么這些屬性在Go中如何查看呢?
package main
import (
"fmt"
"os"
)
func main() {
//打開文件使用os.Open函數,會返回一個文件句柄和一個error
file, err := os.Open(`1.txt`)
if err != nil {
fmt.Println("文件打開失敗:",err)
}
//調用file.Stat()可以查看文件的信息,這是一個os.FileInfo對象
info, err := file.Stat()
if err != nil {
panic(err)
}
//屬性如下
/*
type FileInfo interface {
Name() string // 文件名
Size() int64 // 文件的大小,按字節計算
Mode() FileMode // 文件的模式
ModTime() time.Time // 修改時間
IsDir() bool // 是否是目錄
Sys() interface{} // 數據源,一般不用管
}
*/
fmt.Println(info.Name()) // 1.txt
fmt.Println(info.Size()) // 21
//有點類似於linux里面的,第一個-表示文本文件,后面的三個rw-表示可讀可寫不可執行。
//分別是用戶、用戶所屬組、其他組的權限
fmt.Println(info.Mode()) // -rw-rw-rw-
fmt.Println(info.ModTime()) // 2019-12-18 19:52:44.3146692 +0800 CST
fmt.Println(info.IsDir()) // false
fmt.Println(info.Sys()) // &{32 {1621963324 30848071} {664622106 30848084} {664622106 30848084} 0 21}
}
使用bufio庫來讀取文件
bufio相當於是在os.OpenFile得到的文件句柄之上進行一層封裝,bufio,從名字上也能看出來是帶緩存的io。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, _ := os.Open(`1.txt`)
//使用bufio.NewReader進行一層包裝
reader := bufio.NewReader(file)
//首先這個reader,可以像file一樣,使用Read方法
//然而reader還可是按照指定字符來讀,比如我想一行一行讀,就可以指定換行符來讀
for {
//返回string和error
s, err := reader.ReadString('\n') //表示每讀到\n就停止
//這里使用print,因為文件本身有換行符,println自帶換行,所以使用print
fmt.Print("讀取一行:", s)
/*
讀取一行:白色相簿什么的,已經無所謂了。
讀取一行:因為已經不再有歌,值得去唱了。
讀取一行:傳達不了的戀情,已經不需要了。
讀取一行:因為已經不再有人,值得去愛了。
*/
if err != nil {
if err == io.EOF {
break
} else {
panic(err)
}
}
}
}
除了NewReader,還有一個NewScanner:
package main
import (
"bufio"
"bytes"
"fmt"
"os"
)
func main() {
file, _ := os.Open(`1.txt`)
scanner := bufio.NewScanner(file)
for scanner.Scan(){
//可以看到我們使用println打印,行與行之間沒有空隙,說明scanner.Text()沒有把換行符讀進去。
fmt.Println(scanner.Text())
/*
白色相簿什么的,已經無所謂了。
因為已經不再有歌,值得去唱了。
傳達不了的戀情,已經不需要了。
因為已經不再有人,值得去愛了。
*/
}
//但是這都是一行一行讀的,我們可以寫到緩存里面去
//這個緩存使用bytes.Buffer{}來創建
buf := bytes.Buffer{}
file, _ = os.Open(`1.txt`)
scanner = bufio.NewScanner(file)
for scanner.Scan(){
//直接調用buf.WriteString即可, 當然還可以使用buf.Write寫入字節
buf.WriteString(scanner.Text())
//scanner.Text()得到字符串, 也可以使用scanner.Bytes()得到字節
}
//當調用buf.String()拿到String
//也可以調用buf.Bytes()拿到字節
fmt.Println(buf.String()) // 白色相簿什么的,已經無所謂了。因為已經不再有歌,值得去唱了。傳達不了的戀情,已經不需要了。因為已經不再有人,值得去愛了。
//因為沒有讀取換行符,所以是連在一起的
//這里再提一下bytes.Buffer{},這是一個非常方便的字節緩存。用來組合字符串非常方便
buffer := bytes.Buffer{}
buffer.WriteString("哈哈")
buffer.WriteString("呱呱")
buffer.Write([]byte("嘻嘻"))
fmt.Println(buffer.String()) // 哈哈呱呱嘻嘻
fmt.Println(buffer.Bytes()) // [229 147 136 229 147 136 229 145 177 229 145 177 229 152 187 229 152 187]
//當然buffer也有ReadString,和Read方法
s, _ := buffer.ReadString('\n')
fmt.Println(s) // 哈哈呱呱嘻嘻
buffer.WriteString("你胸大,你先說")
b := make([]byte, 1024)
n, _ := buffer.Read(b)
fmt.Println(string(b[: n])) // 你胸大,你先說
}
使用ioutil模塊
使用ioutil讀取文件更加的方便,我們來看一下:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
file, _ := os.Open(`1.txt`)
// 直接將file丟進去,可以讀取全部內容
s, _ := ioutil.ReadAll(file)
fmt.Println(string(s))
/*
白色相簿什么的,已經無所謂了。
因為已經不再有歌,值得去唱了。
傳達不了的戀情,已經不需要了。
因為已經不再有人,值得去愛了。
*/
// 是不是很方便呢?其實還有更方便的
//調用ReadFile,傳入文件名直接得到字節數組和error。當然底層是使用了os.Open和ioutil.ReadAll
s, _ = ioutil.ReadFile(`1.txt`)
fmt.Println(string(s))
/*
白色相簿什么的,已經無所謂了。
因為已經不再有歌,值得去唱了。
傳達不了的戀情,已經不需要了。
因為已經不再有人,值得去愛了。
*/
}
當然ioutil開可以寫入文件:
package main
import (
"bytes"
"fmt"
"io/ioutil"
)
func main() {
buf := bytes.Buffer{}
buf.WriteString("明明是我先來的\n")
buf.WriteString("為什么會變成這樣呢\n")
buf.WriteString("何でそんなに慣れてんだよ\n")
buf.WriteString("雪菜と何度もキースしたんだよ")
//傳入filename,content,權限
_ = ioutil.WriteFile(`1.txt`, buf.Bytes(), 0666)
//讀取出來看看
s, _ := ioutil.ReadFile(`1.txt`)
fmt.Println(string(s))
/*
明明是我先來的
為什么會變成這樣呢
何でそんなに慣れてんだよ
雪菜と何度もキースしたんだよ
*/
}
之前說ioutil很強大,是因為還有別的用處,比如讀取目錄:
package main
import (
"fmt"
"io/ioutil"
)
func main() {
files, _ := ioutil.ReadDir(`C:\go`)
//返回一個[]os.FileInfo和error
for _, file := range files {
if file.IsDir() {
fmt.Println(file.Name(), "是目錄")
} else {
fmt.Println(file.Name(), "是文件")
}
/*
AUTHORS 是文件
CONTRIBUTING.md 是文件
CONTRIBUTORS 是文件
LICENSE 是文件
PATENTS 是文件
README.md 是文件
SECURITY.md 是文件
VERSION 是文件
api 是目錄
bin 是目錄
doc 是目錄
favicon.ico 是文件
lib 是目錄
misc 是目錄
pkg 是目錄
robots.txt 是文件
src 是目錄
test 是目錄
*/
}
}
讀取命令行
下面來看看如何從命令行中讀取參數:
package main
import (
"fmt"
"os"
)
func main() {
// 會返回一個[]string
args := os.Args
// 命令行輸入: go run 1.go 夏色祭 16 "夏哥 祭妹"
for _, v := range args[1:]{
// 第一個參數是可執行文件名, 我們不用管
fmt.Println(v)
/*
夏色祭
16
夏哥 祭妹
*/
}
}
這種方式非常簡單,但是功能也不強,因為它默認所有參數都是字符串。於是我們可以使用一個標准庫,叫做flag,來看看用法。
package main
import (
"flag"
"fmt"
)
func main() {
//flag.Type里面接受三個參數,指定的名字,默認值,描述信息
Name := flag.String("name", "mashiro", "姓名")
Age := flag.Int("age", 16, "年齡")
Has_bf := flag.Bool("has_bf", false, "有男朋友?")
//解析
flag.Parse()
/*
我們在命令行中便可以通過name satori age 16 has_bf false這種方式指定
當然還可以使用其他方式比如 --name satori、 -name satori、 --name=satori、 -name=satori
*/
//但是注意:得到的Name、Age、Has_bf都是指針
fmt.Println(*Name, *Age, *Has_bf)
}
flag的其他參數
package main
import (
"flag"
"fmt"
)
func main() {
Name := flag.String("name", "mashiro", "姓名")
Age := flag.Int("age", 16, "年齡")
Has_bf := flag.Bool("has_bf", false, "有男朋友?")
flag.Parse()
fmt.Println(*Name, *Age, *Has_bf)
//命令行后的其他參數,是一個[]string類型
fmt.Println(flag.Args())
//命令行后的其他參數個數
fmt.Println(flag.NArg())
//命令行使用的參數個數
fmt.Println(flag.NFlag())
}
我們后面多指定了a b c 1
,所以可以通過flag.Args來獲取,flag.NArgs就是多指定的參數個數,flag.NFlag則是我們在命令行,通過指定關鍵字的方式指定的參數個數,這里是2,因為has_bf我們沒有指定,但即便如此也是打印了默認值的。
flag模塊還可以使用一種方式,flag.TypeVar,這和flag.Type是基本類似的。
package main
import (
"flag"
"fmt"
)
func main() {
var Name string
var Age int
var Has_bf bool
flag.StringVar(&Name, "name", "mashiro", "姓名")
flag.IntVar(&Age, "age", 16, "年齡")
flag.BoolVar(&Has_bf, "has_bf", false, "有男朋友?")
flag.Parse()
fmt.Println(Name, Age, Has_bf)
fmt.Println(flag.Args())
fmt.Println(flag.NArg())
fmt.Println(flag.NFlag())
}
flag.Type是直接返回一個指針,flag.TypeVar則是需要先聲明變量,然后把指針傳進去,至於后面的參數、以及結果都是一樣的。
文件路徑處理
這里推薦一個標准庫叫filepath,它在處理文件路徑的時候非常方便。
1. ToSlash(path string) string:將相關平台的路徑分隔符轉為/
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
sep := os.PathSeparator
// 查看當前平台的系統路徑分隔符,windows平台是\
fmt.Println(string(sep)) // \
// 將分割符轉為/
fmt.Println(filepath.ToSlash(`C:\go\bin\go.exe`)) // C:/go/bin/go.exe
// 注意:該函數不在意路徑是否存在,只是當成普通的字符串進行處理
// 比如我輸入一個不存在的路徑也是可以的
fmt.Println(filepath.ToSlash(`C:\go\go\go\bin\go.exe`)) // C:/go/go/go/bin/go.exe
}
2. FromSlash(path string) string:和ToSlash相反,是將/轉化為相關系統的路徑分隔符
package main
import (
"fmt"
"path/filepath"
)
func main() {
fmt.Println(filepath.FromSlash(`C:/python37/python.exe`)) // C:\python37\python.exe
fmt.Println(filepath.FromSlash(`C:\python37/python.exe`)) // C:\python37\python.exe
fmt.Println(filepath.FromSlash(`C:\python37\python.exe////`)) // C:\python37\python.exe\\\\
/*
可以看到,這兩個函數只是簡單的字符串替換罷了,在源碼里面也是這么做的
func FromSlash(path string) string {
if Separator == '/' {
return path
}
return strings.ReplaceAll(path, "/", string(Separator))
}
另外,這兩個函數在linux下面也能用,但是沒有意義,因為linux的路徑分隔符就是/
*/
}
3. Dir(path string) string:獲取path中最后一個分割符前面的部分,類似於Python中的os.path.dirname(path)
package main
import (
"fmt"
"path/filepath"
)
func main() {
path := `c:\python37\python.exe`
fmt.Println(filepath.Dir(path)) // c:\python37
}
4. Base(path string) string:獲取path中最后一個分割符后面的部分,類似於Python中的os.path.basename(path)
package main
import (
"fmt"
"path/filepath"
)
func main() {
path := `c:\python37\python.exe`
fmt.Println(filepath.Base(path)) // python.exe
}
5. Split(path string) (dir, file string):相當於是Dir和Base的組合,得到最后一個分割符的前面和后面兩部分,類似於Python中os.path.split
package main
import (
"fmt"
"path/filepath"
)
func main() {
path := `c:\python37\python.exe`
dir, file := filepath.Split(path)
fmt.Println(dir, file) // c:\python37\ python.exe
}
6. Ext(path string) string:獲取path中的文件擴展名,類似於Python中的os.path.splitext(path)[1]
package main
import (
"fmt"
"path/filepath"
)
func main() {
path := `c:\python37\python.exe`
fmt.Println(filepath.Ext(path)) // .exe
}
7. Rel(basepath, targpath string) (string, error):獲取targpath相對於basepath的路徑,怎么理解呢?就是basepath進行什么樣的操作,才能得到targpath。其中兩者必須都是相對路徑或者絕對路徑
package main
import (
"fmt"
"path/filepath"
)
func main() {
path1 := `c:\python37\python.exe`
path2 := `c:\python37`
p1, err1 := filepath.Rel(path1, path2)
if err1 != nil {
fmt.Println(err1)
} else {
fmt.Println(p1) // ..
}
//得到的結果是..,表示path1組合..就能得到path2
path1 = `c:\python37`
path2 = `c:\python37\python.exe`
p2, err2 := filepath.Rel(path1, path2)
if err2 != nil {
fmt.Println(err2)
} else {
fmt.Println(p2) // python.exe
}
// 表示path1再組合python.exe就能得到path2
path1 = `c:\python37`
path2 = `c:\go`
p3, err3 := filepath.Rel(path1, path2)
if err3 != nil {
fmt.Println(err3)
} else {
fmt.Println(p3) // ..\go
}
path1 = `c:\python37`
path2 = `d:\overwatch`
p4, err4 := filepath.Rel(path1, path2)
if err4 != nil {
fmt.Println(err4) // Rel: can't make d:\overwatch relative to c:\python37
} else {
fmt.Println(p4)
}
//在window上,如果跨越了盤符,是沒有辦法的
}
8. Join(elem ...string) string:將多個路徑進行組合
package main
import (
"fmt"
"path/filepath"
)
func main() {
path1 := `c:\python37`
path2 := `lib`
path3 := `site-packages`
path4 := `tornado`
fmt.Println(filepath.Join(path1, path2, path3, path4)) // c:\python37\lib\site-packages\tornado
fmt.Println(filepath.Join(path1, path2, "scripts", "pip")) // c:\python37\lib\scripts\pip
fmt.Println(filepath.Join(path1, path2, "..", "scripts", "pip")) // c:\python37\scripts\pip
// ..相當於上一層目錄
}
9. Clean(path string) string:清理路徑中的多余字符
package main
import (
"fmt"
"path/filepath"
)
func main() {
path := `c:\python\\\python37\..\\\python.ext\\\aaa\.\::::\\xx`
fmt.Println(filepath.Clean(path)) // c:\python\python.ext\aaa\::::\xx
// 清除的主要是\\\和\\, 都換成了\,個人覺得不是很常用
}
10. Abs(path string) (string, error):獲取path的絕對路徑,類似於python中的os.path.abspath
package main
import (
"fmt"
"path/filepath"
)
func main() {
path1 := `.`
abs_path1, err1 := filepath.Abs(path1)
if err1 != nil {
fmt.Println(err1)
} else {
fmt.Println(abs_path1) // D:\go
}
//如果是點的話,那么默認是當前go文件所在的工作區的目錄
}
11. IsAbs(path string) bool:判斷當前路徑是否是絕對路徑
package main
import (
"fmt"
"path/filepath"
)
func main() {
path1 := `.`
flag := filepath.IsAbs(path1)
fmt.Println(flag) // false
fmt.Println(filepath.IsAbs(`c:\python37\python.exe`)) // true
}
12. VolumeName(path string) string:返回path所在的卷名,比如c:\python37\python.exe就是c:,而linux中/opt/python/bin就是/opt/python
package main
import (
"fmt"
"path/filepath"
)
func main() {
path1 := `c:\python37\python.exe`
fmt.Println(filepath.VolumeName(path1)) // c:
}
13. EvalSymlinks(path string) (string, error):返回當前鏈接(快捷方式)所指向的文件
14. Match(pattern, name string) (matched bool, err error),判斷 name 是否和指定的模式 pattern 完全匹配,有點類似於Python中的fnmatch,但是匹配的方式不太一樣
package main
import (
"fmt"
"path/filepath"
)
func main() {
/*
?:匹配單個任意字符
*:匹配任意個任意字符
[]:匹配某個范圍內的任意一個字符
[^]:匹配某個范圍外的任意一個字符
[a-z]:也可以使用類似於正則表達式的模式,使用-表示一個區間
\:用來進行轉義,匹配實際的字符,比如\*表示*, \[匹配[, 但是?是不需要轉義的,因為?表示匹配任意一個字符,所以也包含?
[]里面也可以直接包含[ ? *等特殊字符,無需轉義。但是] -這些字符則必須要轉義,必須是\]或者\-才能匹配]和-
*/
fmt.Println(filepath.Match(`???`, "abc")) // true <nil>
fmt.Println(filepath.Match(`???`, "???")) // true <nil>
fmt.Println(filepath.Match(`???`, "abcd")) // false <nil>
fmt.Println(filepath.Match(`*`, "abc")) // true <nil>
fmt.Println(filepath.Match(`*`, "")) // true <nil>
fmt.Println(filepath.Match(`???[?`, "abc[e")) // false syntax error in pattern
fmt.Println(filepath.Match(`[aA][bB][cC]`, `aBc`)) // true <nil>
fmt.Println(filepath.Match(`[^aA]*`, `abc`)) // false <nil>
fmt.Println(filepath.Match(`[a-z]*`, `a+b`)) // true <nil>
}
15. Glob(pattern string) (matches []string, err error),列出與指定的模式 pattern 完全匹配的文件或目錄(匹配原則同Match)
package main
import (
"fmt"
"io/ioutil"
"path/filepath"
)
func main() {
list, err := filepath.Glob(`c:\python37\*`)
if err != nil {
fmt.Println(err)
} else {
for _, v := range list {
fmt.Println(v)
/*
c:\python37\DLLs
c:\python37\Doc
c:\python37\LICENSE.txt
c:\python37\Lib
c:\python37\NEWS.txt
c:\python37\Scripts
c:\python37\Tools
c:\python37\cx_Oracle-doc
c:\python37\etc
c:\python37\include
c:\python37\libs
c:\python37\man
c:\python37\python.exe
c:\python37\python3.dll
c:\python37\python37.dll
c:\python37\pythonw.exe
c:\python37\share
c:\python37\tcl
c:\python37\vcruntime140.dll
*/
}
}
//遍歷文件還有另一種方式
fileinfo, err := ioutil.ReadDir(`c:\python37`)
if err != nil {
fmt.Println(err)
} else {
// fileinfo則是一個切片,里面的數據類型是os.FileInfo
/*
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() interface{} // underlying data source (can return nil)
}
*/
for _, v := range fileinfo {
message := fmt.Sprintf("文件名:%s,大小:%d,是否是目錄,%t", v.Name(), v.Size(), v.IsDir())
fmt.Println(message)
/*
文件名:DLLs,大小:0,是否是目錄,true
文件名:Doc,大小:0,是否是目錄,true
文件名:LICENSE.txt,大小:30195,是否是目錄,false
文件名:Lib,大小:0,是否是目錄,true
文件名:NEWS.txt,大小:665470,是否是目錄,false
文件名:Scripts,大小:0,是否是目錄,true
文件名:Tools,大小:0,是否是目錄,true
文件名:cx_Oracle-doc,大小:0,是否是目錄,true
文件名:etc,大小:0,是否是目錄,true
文件名:include,大小:0,是否是目錄,true
文件名:libs,大小:0,是否是目錄,true
文件名:man,大小:0,是否是目錄,true
文件名:python.exe,大小:99856,是否是目錄,false
文件名:python3.dll,大小:58896,是否是目錄,false
文件名:python37.dll,大小:3748368,是否是目錄,false
文件名:pythonw.exe,大小:98320,是否是目錄,false
文件名:share,大小:0,是否是目錄,true
文件名:tcl,大小:0,是否是目錄,true
文件名:vcruntime140.dll,大小:89752,是否是目錄,false
*/
}
}
}
16. Walk(root string, walkFn WalkFunc) error:遍歷指定目錄下的所有子目錄,對遍歷的項目使用walkFn進行處理,WalkFunc類型如下。
type WalkFunc func(path string, info os.FileInfo, err error) error
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
walkFn := func(path string, info os.FileInfo, err error) error {
//遍歷的時候,會自動將path:文件路徑、info:文件信息、err:錯誤傳遞進來,我們可以寫上自己的邏輯
//獲取文件名
filename := info.Name()
if strings.HasSuffix(filename, "__init__.py") {
//如果文件是以__init__.py結尾的話, 直接打印對應的path
fmt.Println(path)
}
//這里我們直接返回nil即可
return nil
}
//那么開始遍歷,會對遍歷的每一個內容都交給walkFn進行處理
err := filepath.Walk(`c:\python37\lib\site-packages\numpy`, walkFn)
if err != nil {
fmt.Println("err =", err)
}
/*
輸出結果
c:\python37\lib\site-packages\numpy\__init__.py
c:\python37\lib\site-packages\numpy\compat\__init__.py
c:\python37\lib\site-packages\numpy\compat\tests\__init__.py
c:\python37\lib\site-packages\numpy\core\__init__.py
c:\python37\lib\site-packages\numpy\core\tests\__init__.py
c:\python37\lib\site-packages\numpy\distutils\__init__.py
c:\python37\lib\site-packages\numpy\distutils\command\__init__.py
c:\python37\lib\site-packages\numpy\distutils\fcompiler\__init__.py
c:\python37\lib\site-packages\numpy\distutils\tests\__init__.py
c:\python37\lib\site-packages\numpy\doc\__init__.py
c:\python37\lib\site-packages\numpy\f2py\__init__.py
c:\python37\lib\site-packages\numpy\f2py\tests\__init__.py
c:\python37\lib\site-packages\numpy\fft\__init__.py
c:\python37\lib\site-packages\numpy\fft\tests\__init__.py
c:\python37\lib\site-packages\numpy\lib\__init__.py
c:\python37\lib\site-packages\numpy\lib\tests\__init__.py
c:\python37\lib\site-packages\numpy\linalg\__init__.py
c:\python37\lib\site-packages\numpy\linalg\tests\__init__.py
c:\python37\lib\site-packages\numpy\ma\__init__.py
c:\python37\lib\site-packages\numpy\ma\tests\__init__.py
c:\python37\lib\site-packages\numpy\matrixlib\__init__.py
c:\python37\lib\site-packages\numpy\matrixlib\tests\__init__.py
c:\python37\lib\site-packages\numpy\polynomial\__init__.py
c:\python37\lib\site-packages\numpy\polynomial\tests\__init__.py
c:\python37\lib\site-packages\numpy\random\__init__.py
c:\python37\lib\site-packages\numpy\random\tests\__init__.py
c:\python37\lib\site-packages\numpy\testing\__init__.py
c:\python37\lib\site-packages\numpy\testing\_private\__init__.py
c:\python37\lib\site-packages\numpy\testing\tests\__init__.py
c:\python37\lib\site-packages\numpy\tests\__init__.py
*/
}
統計一下文件個數,分為兩個版本,Go版和Python版
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
func main() {
start_time := time.Now().UnixNano()
//下面我們來統計一下,c:\python37下面有多少個__init__.py文件,有多少個py文件,有多少個不是py文件的文件,有多少個目錄
init_py_file := 0
py_file := 0
not_py_file := 0
dir := 0
walkFn := func(path string, info os.FileInfo, err error) error {
filename := info.Name()
is_dir := info.IsDir()
if strings.HasSuffix(filename, "__init__.py") {
//如果文件是以__init__.py結尾的話, 別忘了py_file也要++
init_py_file++
py_file++
} else if strings.HasSuffix(filename, ".py") {
//以.py結尾的話
py_file++
} else if is_dir {
//如果是目錄的話
dir++
} else {
//既不是py結尾,也不是目錄的話
not_py_file++
}
//這里我們直接返回nil即可
return nil
}
//那么開始遍歷,會對遍歷的每一個內容都交給walkFn進行處理
err := filepath.Walk(`c:\python37`, walkFn)
if err != nil {
fmt.Println("err =", err)
}
//打印相應的個數
fmt.Println(fmt.Sprintf("__init__.py文件個數:%d,py文件個數:%d,不是py文件的文件個數:%d,目錄個數:%d", init_py_file, py_file, not_py_file, dir))
/*
__init__.py文件個數:1965,py文件個數:11911,不是py文件的文件個數:23050,目錄個數:7685
*/
end_time := time.Now().UnixNano()
fmt.Println("總用時:", (end_time-start_time)/1e9) // 總用時: 4
}
再來看看Python版本的
from pathlib import Path
import os
import time
start_time = time.perf_counter()
init_py_file = 0
py_file = 0
not_py_file = 0
dir_ = 0
p = Path(r"c:\python37")
for file in p.rglob("*"):
if str(file).endswith("__init__.py"):
init_py_file += 1
py_file += 1
elif str(file).endswith(".py"):
py_file += 1
elif os.path.isdir(str(file)):
dir_ += 1
else:
not_py_file += 1
end_time = time.perf_counter()
print(f"__init__.py文件個數:{init_py_file},py文件個數:{py_file},不是py文件的文件個數:{not_py_file},目錄個數:{dir_}")
# __init__.py文件個數:1965,py文件個數:11911,不是py文件的文件個數:23050,目錄個數:7684
print(f"總用時:{end_time - start_time}")
# 總用時:3.695507357
總結:兩者之間效率基本上是一樣的,代碼量的話,由於golang的{}多占了一行,並且如果不考慮if err != nil之類的話,代碼量也基本差不多。但是我們比較一下結果的話,其他的是一樣的,只有目錄的個數不一樣,Go的結果貌似比Python多了一個,這是因為,我們遞歸遍歷的是c:\python37。在Go中,把c:\python37這個目錄本身也給算進去了,而Python沒有。所以其他的結果是一致的,但是目錄個數的話,Go算出來會比Python多一個。當然仔細觀察結果的話,稍加思考就能發現原因,這里程序就不再演示了,可以找一個文件或目錄數量比較少的打印一下,會發現Python沒有把最外層的目錄打印出來,打印都是指定目錄里面的內容。
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
walkFn := func(path string, info os.FileInfo, err error) error {
is_dir := info.IsDir()
if is_dir {
//如果是目錄,那么我們就跳過,不做處理
fmt.Println(fmt.Sprintf("%s是目錄,已經跳過", path))
return filepath.SkipDir
}
return nil
}
//那么開始遍歷,會對遍歷的每一個內容都交給walkFn進行處理
err := filepath.Walk(`c:\python37`, walkFn)
if err != nil {
fmt.Println("err =", err)
}
/*
c:\python37是目錄,已經跳過
*/
//可以發現,上來就跳過了,跳過是指整個目錄跳過,目錄里面有什么也不再關心了
//也可以得出,Go是會處理指定的目錄,而Python不會,Python只處理指定目錄里面的內容
}