golang自带库使用说明
Golang中与时间有关的操作,主要涉及到 time 包,核心数据结构是 time.Time
,如下:
type Time struct {
wall uint64
ext int64
loc *Location
}
1、获取时间相关函数
1.1 获取当前时间
// 返回当前时间,注意此时返回的是 time.Time 类型
now := time.Now()
fmt.Println(now)
// 当前时间戳
fmt.Println(now.Unix())
// 纳秒级时间戳
fmt.Println(now.UnixNano())
// 时间戳小数部分 单位:纳秒
fmt.Println(now.Nanosecond())
输出:
2021-01-10 14:56:15.930562 +0800 CST m=+0.000124449
1610261775
1610261775930562000
930562000
1.2 返回当前年月日时分秒、星期几、一年中的第几天等操作
now := time.Now()
// 返回日期
year, month, day := now.Date()
fmt.Printf("year:%d, month:%d, day:%d\n", year, month, day)
// 年
fmt.Println(now.Year())
// 月
fmt.Println(now.Month())
// 日
fmt.Println(now.Day())
// 时分秒
hour, minute, second := now.Clock()
fmt.Printf("hour:%d, minute:%d, second:%d\n", hour, minute, second)
// 时
fmt.Println(now.Hour())
// 分
fmt.Println(now.Minute())
// 秒
fmt.Println(now.Second())
// 返回星期
fmt.Println(now.Weekday())
//返回一年中对应的第几天
fmt.Println(now.YearDay())
//返回时区
fmt.Println(now.Location())
// 返回一年中第几天
fmt.Println(now.YearDay())
1.3 格式化时间
Go 语言提供了时间类型格式化函数 Format()
,需要注意的是 Go 语言格式化时间模板不是常见的 Y-m-d H:i:s
,而是 2006-01-02 15:04:05,也很好记忆(2006 1 2 3 4 5)。
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("15:04:05"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
2、时间戳与日期字符串相互转化
时间戳转成日期格式,需要先转成将时间戳转成 time.Time
类型再格式化成日期格式。
2.1 根据秒数、纳秒数返回 time.Time
类型
now := time.Now()
layout := "2006-01-02 15:04:05"
t := time.Unix(now.Unix(),0) // 参数分别是:秒数,纳秒数
fmt.Println(t.Format(layout))
2.2 根据指定时间返回 time.Time
类型,使用函数 time.Date()
now := time.Now()
layout := "2006-01-02 15:04:05"
//根据指定时间返回 time.Time 类型
//分别指定年,月,日,时,分,秒,纳秒,时区
t := time.Date(2011, time.Month(3), 12, 15, 30, 20, 0, now.Location())
fmt.Println(t.Format(layout))
2.3 日期字符串解析成 time.Time
类型
t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:04:05"), time.Local)
fmt.Println(t)
// 输出 2021-01-10 17:28:50 +0800 CST
// time.Local 指定本地时间
解析的时候需要特别注意时区的问题:
fmt.Println(time.Now())
fmt.Println(time.Now().Location())
t, _ := time.Parse("2006-01-02 15:04:05", "2021-01-10 15:01:02")
fmt.Println(t)
输出:
2021-01-10 17:22:10.951904 +0800 CST m=+0.000094166
Local
2021-01-10 15:01:02 +0000 UTC
可以看到,time.Now()
使用的 CST(中国标准时间),而 time.Parse()
默认的是 UTC(零时区),它们相差 8 小时。所以解析时常用 time.ParseInLocation()
,可以指定时区。
3、计算、比较日期
讲到日期的计算就不得不提 time 包提供的一种新的类型 Duration
,源码是这样定义的:
type Duration int64
底层类型是 int64,表示一段时间间隔,单位是 纳秒。
3.1 24小时之内的时间计算
now := time.Now()
fmt.Println(now)
// 1小时1分1s之后
t1, _ := time.ParseDuration("1h1m1s")
fmt.Println(t1)
m1 := now.Add(t1)
fmt.Println(m1)
// 1小时1分1s之前
t2, _ := time.ParseDuration("-1h1m1s")
m2 := now.Add(t2)
fmt.Println(m2)
// 3小时之前
t3, _ := time.ParseDuration("-1h")
m3 := now.Add(t3 * 3)
fmt.Println(m3)
// 10 分钟之后
t4, _ := time.ParseDuration("10m")
m4 := now.Add(t4)
fmt.Println(m4)
// Sub 计算两个时间差
sub1 := now.Sub(m3)
fmt.Println(sub1.Hours()) // 相差小时数
fmt.Println(sub1.Minutes()) // 相差分钟数
额外再介绍两个函数 time.Since()
、time.Until()
:
// 返回当前时间与 t 的时间差,返回值是 Duration
time.Since(t Time) Duration
// 返回 t 与当前时间的时间差,返回值是 Duration
time.Until(t Time) Duration
now := time.Now()
fmt.Println(now)
t1, _ := time.ParseDuration("-1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(time.Since(m1))
fmt.Println(time.Until(m1))
输出:
2021-01-10 20:41:48.668232 +0800 CST m=+0.000095594
2021-01-10 19:41:48.668232 +0800 CST m=-3599.999904406
1h0m0.000199007s
-1h0m0.000203035s
3.2 24小时之外的时间计算
涉及到一天以外的时间计算,就需要用到 time.AddDate()
,函数原型:
func (t Time) AddDate(years int, months int, days int) Time
比如想知道 一年一个月零一天 之后的时间,就可以这样:
now := time.Now()
fmt.Println(now)
m1 := now.AddDate(1,1,1)
fmt.Println(m1)
再比如,想获得 2 天之前时间:
now := time.Now()
fmt.Println(now)
m1 := now.AddDate(0,0,-2)
fmt.Println(m1)
3.3 日期比较
日期的比较总共有三种:之前、之后和相等。
// 如果 t 代表的时间点在 u 之前,返回真;否则返回假。
func (t Time) Before(u Time) bool
// 如果 t 代表的时间点在 u 之后,返回真;否则返回假。
func (t Time) After(u Time) bool
// 比较时间是否相等,相等返回真;否则返回假。
func (t Time) Equal(u Time) bool
now := time.Now()
fmt.Println(now)
// 1小时之后
t1, _ := time.ParseDuration("1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(m1.After(now))
fmt.Println(now.Before(m1))
fmt.Println(now.Equal(m1))
输出:
2021-01-10 21:00:44.409785 +0800 CST m=+0.000186800
2021-01-10 22:00:44.409785 +0800 CST m=+3600.000186800
true
true
false
4、常见例子
下面列举一些常见的例子和函数封装。
4.1 日期格式 转 时间戳
func TimeStr2Time(fmtStr,valueStr, locStr string) int64 {
loc := time.Local
if locStr != "" {
loc, _ = time.LoadLocation(locStr) // 设置时区
}
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
t, _ := time.ParseInLocation(fmtStr, valueStr, loc)
return t.Unix()
}
4.2 获取当前时间日期格式
func GetCurrentFormatStr(fmtStr string) string {
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
return time.Now().Format(fmtStr)
}
4.3 时间戳 to 日期格式
func Sec2TimeStr(sec int64, fmtStr string) string {
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
return time.Unix(sec, 0).Format(fmtStr)
}
Go-regexp正则
package main
import (
"fmt"
"regexp"
)
const text = "My email is ccmouse@gmail.com"
func main() {
compile := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+`)
match := compile.FindString(text)
fmt.Println(match)
}
Go存储基础 — 文件 IO 操作
两大 IO 分类
计算的体系架构,CPU,内存,网络,IO。那么 IO 是什么呢?一般理解成 Input、Output 的缩写,通俗话就是输入输出的意思。
IO 分为网络和存储 IO 两种类型(其实网络 IO 和磁盘 IO 在 Go 里面有着根本性区别)。网络 IO 对应的是网络数据传输过程,网络是分布式系统的基石,通过网络把离散的物理节点连接起来,形成一个有机的系统。
存储 IO 对应的就是数据存储到物理介质的过程,通常物理介质对应的是磁盘,磁盘上一般会分个区,然后在上面格式化个文件系统出来,所以普通程序员最常看见的是文件 IO 的形式。
在 Golang 里可以归类出两种读写文件的方式:
-
标准库封装:操作对象
File
; -
系统调用 :操作对象
fd
;
读写数据要素
文件的读写最核心的要素是什么?
通俗来讲:读文件,就是把磁盘上的文件的特定位置的数据读到内存的 buffer 。写文件,就是把内存 buffer 的数据写到磁盘的文件的特定位置。
这里注意到两个关键词:
-
特定位置;
-
内存 buffer;
特定位置怎么理解?怎么指定所谓的特定位置
?
很简单,用 [ offset, length ]
这两个参数就能标识一段位置。
也就是 IO 偏移和长度,Offset 和 Length。
内存 buffer 怎么理解?
归根结底,文件的数据和谁直接打交道?内存,写的时候是从内存写到磁盘文件的,读的时候是从磁盘文件读到内存的。
本质上,下面的 IO 函数都离不开 Offset,Length,buffer 这三个要素。
标准库封装
Go 对文件进行读写非常简单,因为 Go 已经封装了一个非常便捷的使用接口,位于标准库 os 中。Go 标准库对文件 IO 的封装也就是 Go 推荐对文件进行 IO 时使用的操作方式。
打开文件(Open)
func OpenFile(name string, flag int, perm FileMode) (*File, error)
Open 文件之后,获取到一个句柄,也就是 File
结构,之后对文件的读写都是基于 File
结构之上进行的。
type File struct {
*file // os specific
}
文件读写只需要针对这个句柄结构体做操作即可。
另外有一点隐藏起来的知识点必须要提一下:偏移。也就是最开始强调的读写 3 要素之一的 Offset 。打开(Open
)文件的时候,文件当前偏移量默认设置为 0,也就是说 IO 的起始位置就是文件的最开头。举个例子,如果这个时候,写 4K 的数据到文件,那么就是写 [0, 4K] 这个位置的数据,如果之前这上面已经有数据了,那么就会是覆盖写。
除非 Open
文件的时候指定 O_APPEND
选项,偏移量会设置为文件末尾,那么 IO 都是从文件末尾开始。
文件写操作(Write)
文件 File
句柄对象有两个写方法:
第一种:写一个 buffer 到文件 ,使用文件当前偏移
func (f *File) Write(b []byte) (n int, err error)
注意:该写操作会导致文件偏移量的增加。
第二种:从指定文件偏移,写入 buffer 到文件
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
注意:该写操作不会更新文件偏移量
文件读操作(Read)
和写对应,文件 File
句柄对象有两个读方法:
第一种:从文件当前偏移读一个 buffer 的数据上来
func (f *File) Read(b []byte) (n int, err error)
注意:该读操作会导致文件偏移量的增加。
第二种:从指定文件偏移,读一个 buffer 大小的数据上来
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
注意:该读操作不会更新文件偏移量
指定偏移量(Seek)
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
这个句柄方法允许用户指定文件的偏移位置。这个很容易理解,举个例子,文件刚开始是 0 字节,写 1M 的数据下去,大小变成 1M,Offset 往后挪 1M ,默认就是往后挪。
现在 Seek 方法允许把写的偏移定位到任意位置,可以就可以从任意地方覆盖写入数据。
所以在 Go 里面,文件 IO 非常简单,先 Open 一个文件,拿到 File
句柄,然后就可以使用这个句柄 Write ,Read,Seek 就能进行 IO 了。
底层的原理
Go 的标准库 os
提供了极其方便的封装,深入最原始的本质可以发现最核心的东西:系统调用。
Go 标准库的文件存储 IO 就是基于系统调用之上的。可以稍微跟一下 os.OpenFile
的调用:
os 库的 OpenFile
函数:
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
f, err := openFileNolog(name, flag, perm)
if err != nil {
return nil, err
}
f.appendMode = flag&O_APPEND != 0
return f, nil
}
稍微看下 openFileNolog
函数:
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
var r int
for {
var e error
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e == nil {
break
}
if runtime.GOOS == "darwin" && e == syscall.EINTR {
continue
}
return nil, &PathError{"open", name, e}
}
return newFile(uintptr(r), name, kindOpenFile), nil
}
可以看到 syscall.Open
,这个函数获取到一个整数,也就是在 c 语言里最常见的 fd 句柄,而 File
结构体则仅仅是基于这个的一层封装而已。
思考下,为什么会有标准库封装这一层存在?
划重点:为了屏蔽操作系统的区别,使用这个标准库的所有操作都是跨平台的。换句话说,如果是特殊操作系统才有的特性,那么在 os 库里就找不到对应封装的 IO 操作。
那么怎么使用系统调用?
直接使用 syscall 库,也就是系统调用。从名字也能看出来,系统调用是和操作系统强相关的,因为是操作系统提供的调用接口,所以系统调用会因为操作系统不同而导致不同的特性,不同的接口。
所以,如果直接使用 syscall 库来使用系统调用,那么需要自己来承受系统带来的兼容性问题。
系统调用
系统调用在 syscall 里有一层最基础的封装:
文件 Open
func Open(path string, mode int, perm uint32) (fd int, err error)
文件 Read
func Read(fd int, p []byte) (n int, err error) func Pread(fd int, p []byte, offset int64) (n int, err error)
文件读有两个接口,一个 Read
是从当前默认偏移读一个 buffer 数据,Pread
接口则是从指定位置读数据的接口。
思考一个问题:Pread
从效果上来讲等于 Seek
和 Read
组合起来使用,那么是否可以认为 Pread
就可以被 Seek
+ Read
替代呢?
不行!根本原因在于 Seek
+ Read
是在用户层就是两步操作,而 Pread
虽然是 Seek
+ Read
的效果,但是操作系统给到用户的语义是:Pread
是一个原子操作。还有一个重要区别,Pread
不会改变当前文件的偏移量(普通的 Read
调用会更新偏移量)。
所以,总结下,**Pread**
和顺序调用 **Seek**
后调用 **Read**
有两点重要区别:
-
Pread
对用户提供的语义是原子操作,在调用Pread
时,无法中断Seek
和Read
操作; -
Pread
调用不会更新当前文件偏移量;
文件 Write
func Write(fd int, p []byte) (n int, err error) func Pwrite(fd int, p []byte, offset int64) (n int, err error)
文件写对应也是有两种接口,Wrtie
和 Pwrite
分别是对应 Read
和 Pread
。同样的,Pwrite
作用上也是相当于先调用 Seek
再调用 Write
,但是同样的也有两点不同:
-
Pwrite
完成Seek
和Write
对外是原子操作的语义; -
Pwrite
调用不会更新当前文件偏移量;
文件 Seek
func Seek(fd int, offset int64, whence int) (off int64, err error)
这个函数调用允许用户指定偏移(这个会影响到 Read
和 Write
读写的位置)。一般来说,每个打开文件都有一个相关联的“当前文件偏移量”( current file offset )。读(Read
)、写(Write
)操作都是从当前文件偏移量处开始,并且 Read
和 Write
会导致偏移量增加,增加量就是所读写的字节数。
小结一下:Go核心的 Open,Read,Write,Seek 几个系统调用,可以发现一个明显不同与标准 IO 库的区别:系统调用操作对象是一个整数句柄。Open
文件得到一个整数 fd,之后的所有 IO 都是针对这个 fd 来操作的。这个明显和标准库不同,os 标准库 OpenFile 得到的是一个 File
结构体,所有的 IO 也是针对这个结构体的。
层次架构
那么究竟封装的层次一般是什么样的呢, Unix 编程里面开篇就有一张如下图:
这张图就非常形象的讲明白了整个 Unix 体系结构。
-
内核是最核心的实现,包括了和 IO 设备,硬件交互等功能。与内核紧密的一层是内核提供给外部调用的系统调用,系统调用提供了用户态到内核态调用的一个通道;
-
对于系统调用,各个语言的标准库会有一些封装,比如 C 语言的 libc 库,Go 语言的 os ,syscall 库都是类似的地位,这个就是所谓的公共库。这层封装的作用最主要是简化普通程序员使用效率,并且屏蔽系统细节,为跨平台提供基础(同样的,为了跨平台的特性,可能会阉割很多不兼容的功能,所以才会有直接调用系统掉调用的需求);
-
当然,右上角还看到一个缺口,应用程序除了可以使用公共函数库,其实是可以直接调用系统调用的,但是由此带来的复杂性又应用自己承担。这种需求也是很常见的,标准库封装了通用的东西,同样割舍了很多系统调用的功能,这种情况下,只能通过系统调用来获取;
总结
-
IO 大类分为网络 IO 和磁盘 IO,IO 对文件来说就是读写操作,写的时候数据从内存到磁盘,读的时候数据从磁盘到内存;
-
Go 文件 IO 最常用的是 os 库,使用 Go 封装的标准库,
os.OpenFile
打开,File.Write
,File.Read
进行读写,操作对象都是File
结构体; -
Go 标准库对 IO 的封装是为了屏蔽复杂的系统调用,提供跨平台的使用姿势。然后单独提供
syscall
库,让程序员自我决策使用要使用更丰富的系统调用功能,当然后果自负; -
Go 标准库 IO 操作对象是
File
,系统调用 IO 操作对象是 fd(非负整数)。 -
Open
文件默认当前偏移量是 0 (文件最开始),加上O_APPEND
参数之后偏移量会是文件末尾。通过 Seek 调用可以任意指定文件偏移,从而影响文件 IO 的位置; -
Read
,Write
函数只有 buffer (buffer 有长度),偏移则使用当前文件偏移量; -
Pread
,Pwrite
的系统调用效果等同于Seek
偏移量然后Read
,Write
,但是又大有不同。对外语义是原子操作,并且不更新当前文件偏移量;
Go-文件读写操作
读写文件
package main
import (
"bufio"
"fmt"
"io"
"os"
)
/*在已存在文件清空原有内容进行追加*/
func main() {
filePath := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err = %v\n", err)
return
}
/*关闭文件流*/
defer file.Close()
/*读取*/
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Print(str)
}
/*写入文件*/
str := "hello FCC您好!!!\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*因为writer是带缓存的,需要通过flush到磁盘*/
writer.Flush()
}
文件内容拷贝至新文件
package main
import (
"fmt"
"io/ioutil"
)
/*将文件1的内容拷贝到文件2*/
func main() {
file1Path := "D:\\fcofficework\\DNS\\1.txt"
file2Path := "D:\\fcofficework\\DNS\\2.txt"
data, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Printf("read file err=%v", err)
return
}
err = ioutil.WriteFile(file2Path, data, 0666)
if err != nil {
fmt.Printf("write file err=%v\n", err)
}
}
判断文件或者目录是否存在
package main
import (
"fmt"
"os"
)
/*判断文件以及目录是否存在*/
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
fmt.Println("当前文件存在!")
return true, nil
}
if os.IsNotExist(err) {
fmt.Println("当前文件不存在!")
return false, nil
}
return false, nil
}
func main() {
path := "D:\\fcofficework\\2.txt"
PathExists(path)
}
文件的拷贝
package main
import (
"bufio"
"fmt"
"io"
"os"
)
/*文件的拷贝*/
func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
srcFile, err := os.Open(srcFileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
}
reader := bufio.NewReader(srcFile)
dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
writer := bufio.NewWriter(dstFile)
defer dstFile.Close()
return io.Copy(writer, reader)
}
func main() {
srcFile := "D:\\Photos\\Datapicture\\mmexport1530688562488.jpg"
dstFile := "D:\\Photos\\1.jpg"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Println("拷贝完成!")
} else {
fmt.Println("拷贝失败,err=", err)
}
}
读取文件并统计文件中字符的个数
package main
import (
"bufio"
"fmt"
"io"
"os"
)
/*统计文件的字符个数*/
type CharCount struct {
/*英文的个数*/
ChCount int
/*数字的个数*/
NumCount int
/*空格的个数*/
SpaceCount int
/*其他字符的个数*/
OtherCount int
}
func main() {
fileName := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
defer file.Close()
var count CharCount
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
for _, v := range str {
switch {
case v >= 'a' && v <= 'z':
fallthrough
case v >= 'A' && v <= 'Z':
count.ChCount++
case v == ' ' || v == '\t':
count.SpaceCount++
case v >= '0' && v <= '9':
count.NumCount++