os包中实现了不依赖平台的操作系统函数接口(平台无关的接口),设计向Unix风格,但是错误处理是go风格,当os包使用时,如果失败之后返回错误类型而不是错误数量,返回错误值而非错误码,可以包含更多信息。

os

os 依赖于 syscall。在实际编程中,我们应该总是优先使用 os 中提供的功能,而不是 syscall。

os包提供了操作系统函数的不依赖平台的接口。一般都是linux下的一些基本命令的操作,比如文件,目录操作之类。

我们运行程序常用的命令行参数就是在这个包中可以获取

var Args []string

Args保管了命令行参数,第一个是程序名。

文件io

文件IO就是对文件的读写操作,我们先了解一些os中的基本概念。

基本概念

文件描述符

所有 I/O 操作以文件描述符 ( 一个非负整数 , 通常是小整数 ) 来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。

在 Go 中,文件描述符封装在 os.File 结构中,通过 File.Fd() 可以获得底层的文件描述符:fd。

File结构体

type File struct {
    *file
}
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
    fd      int
    name    string
    dirinfo *dirInfo // nil unless directory being read
}

// Auxiliary information if the File describes a directory
type dirInfo struct {
    buf  []byte // buffer for directory I/O
    nbuf int    // length of buf; return value from Getdirentries
    bufp int    // location of next record in buf.
}

标准定义

按照惯例,大多数程序都期望能够使用 3 种标准的文件描述符:0- 标准输入;1- 标准输出;2- 标准错误。os 包提供了 3 个 File 对象,分别代表这 3 种标准描述符:Stdin、Stdout 和 Stderr,它们对应的文件名分别是:/dev/stdin、/dev/stdout 和 /dev/stderr。

基本操作

创建

func NewFile(fd uintptr, name string) *File

NewFile使用给出的Unix文件描述符和名称创建一个文件。

正常使用create来创建一个文件,比如文件不存在,就创建一个

file,er:=os.Open("xxx")
defer func(){file.Close()}()
if er!=nil && os.IfNotExist(er
r){
  file = os.Create("xx")

}

打开

func Open(name string) (file *File, err error)

Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。

func OpenFile(name string, flag int, perm FileMode) (file *File, err error)

OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。

位掩码参数 flag 用于指定文件的访问模式,可用的值在 os 中定义为常量(以下值并非所有操作系统都可用):

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和 O_CREATE 配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步 I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)

O_TRUNC这个参数可以用来清空文件,如果可以的话,还可以用这个函数

os.Truncate(name, size)

或者

func (f *File) Truncate(size int64) error
size 填0 就把文件清空了。

下面有详细的说明

位掩码参数 perm 指定了文件的模式和权限位,类型是 os.FileMode,文件模式位常量定义在 os 中:

const (
    // 单字符是被 String 方法用于格式化的属性缩写。
    ModeDir        FileMode = 1 << (32 - 1 - iota) // d: 目录
    ModeAppend                                     // a: 只能写入,且只能写入到末尾
    ModeExclusive                                  // l: 用于执行
    ModeTemporary                                  // T: 临时文件(非备份文件)
    ModeSymlink                                    // L: 符号链接(不是快捷方式文件)
    ModeDevice                                     // D: 设备
    ModeNamedPipe                                  // p: 命名管道(FIFO)
    ModeSocket                                     // S: Unix 域 socket
    ModeSetuid                                     // u: 表示文件具有其创建者用户 id 权限
    ModeSetgid                                     // g: 表示文件具有其创建者组 id 的权限
    ModeCharDevice                                 // c: 字符设备,需已设置 ModeDevice
    ModeSticky                                     // t: 只有 root/ 创建者能删除 / 移动文件

    // 覆盖所有类型位(用于通过 & 获取类型位),对普通文件,所有这些位都不应被设置
    ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
    ModePerm FileMode = 0777 // 覆盖所有 Unix 权限位(用于通过 & 获取类型位)
)

read

func (f *File) Read(b []byte) (n int, err error)

Read 方法从 f 中读取最多 len(b) 字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取 0 个字节且返回值 err 为 io.EOF。

从方法声明可以知道,File 实现了 io.Reader 接口。

func (f *File) ReadAt(b []byte, off int64) (n int, err error)

ReadAt 从指定的位置(相对于文件开始位置)读取长度为 len(b) 个字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误。当 n<len(b) 时,本方法总是会返回错误;如果是因为到达文件结尾,返回值 err 会是 io.EOF。它对应的系统调用是 pread。

实例

var chunks []byte
buf := make([]byte, 1024)
var count = 0
for {
    n, err := f.Read(buf)
    if err != nil && err != io.EOF {
        panic(err)
    }
    if 0 == n {
        break
    }
    count = count + n
    chunks = append(chunks, buf[:n]...)
}
r.logger.Debugf("read file content : %s",string(chunks[:count]))

这边这个实例主要是要说明一下几个重点:

1、buf必须make,不然会panic
2、read必须for循环,直到io.EOF

write

func (f *File) Write(b []byte) (n int, err error)

Write 向文件中写入 len(b) 字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值 n!=len(b),本方法会返回一个非 nil 的错误。

从方法声明可以知道,File 实现了 io.Writer 接口。

Write 与 WriteAt 的区别同 Read 与 ReadAt 的区别一样。为了方便,还提供了 WriteString 方法,它实际是对 Write 的封装。

注意:Write 调用成功并不能保证数据已经写入磁盘,因为内核会缓存磁盘的 I/O 操作。如果希望立刻将数据写入磁盘(一般场景不建议这么做,因为会影响性能),有两种办法:

1. 打开文件时指定 `os.O_SYNC`;
2. 调用 `File.Sync()` 方法。

说明:File.Sync() 底层调用的是 fsync 系统调用,这会将数据和元数据都刷到磁盘;如果只想刷数据到磁盘(比如,文件大小没变,只是变了文件数据),需要自己封装,调用 fdatasync(syscall.Fdatasync) 系统调用。

close

close() 系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当进程终止时,将自动关闭其已打开的所有文件描述符。

func (f *File) Close() error

os.File.Close() 是对 close() 的封装。我们应该养成关闭不需要的文件的良好编程习惯。文件描述符是资源,Go 的 gc 是针对内存的,并不会自动回收资源,如果不关闭文件描述符,长期运行的服务可能会把文件描述符耗尽。

以下两种情况会导致 Close 返回错误:

1. 关闭一个未打开的文件;
2. 两次关闭同一个文件;

通常,我们不会去检查 Close 的错误

seek

func (f *File) Seek(offset int64, whence int) (ret int64, err error)

Seek 设置下一次读 / 写的位置。offset 为相对偏移量,而 whence 决定相对位置:0 为相对文件开头,1 为相对当前位置,2 为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误。使用中,whence 应该使用 os 包中的常量:SEEK_SET、SEEK_CUR 和 SEEK_END

实例

file.Seek(0, os.SEEK_SET)    // 文件开始处
file.Seek(0, SEEK_END)        // 文件结尾处的下一个字节
file.Seek(-1, SEEK_END)        // 文件最后一个字节
file.Seek(-10, SEEK_CUR)     // 当前位置前 10 个字节
file.Seek(1000, SEEK_END)    // 文件结尾处的下 1001 个字节

trucate

trucate 和 ftruncate 系统调用将文件大小设置为 size 参数指定的值;Go 语言中相应的包装函数是 os.Truncate 和 os.File.Truncate。

func Truncate(name string, size int64) error
func (f *File) Truncate(size int64) error

如果文件当前长度大于参数 size,调用将丢弃超出部分,若小于参数 size,调用将在文件尾部添加一系列空字节或是一个文件空洞。

remove

func Remove(name string) error

Remove删除name指定的文件或目录。如果出错,会返回*PathError底层类型的错误。

文件属性

文件信息

可以通过包里的函数 Stat、Lstat 和 File.Stat 可以得到os.FileInfo 接口的信息。这三个函数对应三个系统调用:stat、lstat 和 fstat。

这三个函数的区别:

  • stat 会返回所命名文件的相关信息。
  • lstat 与 stat 类似,区别在于如果文件是符号链接,那么所返回的信息针对的是符号链接自身(而非符号链接所指向的文件)。
  • fstat 则会返回由某个打开文件描述符(Go 中则是当前打开文件 File)所指代文件的相关信息。

Stat 和 Lstat 无需对其所操作的文件本身拥有任何权限,但针对指定 name 的父目录要有执行(搜索)权限。而只要 File 对象 ok,File.Stat 总是成功。

func (f *File) Stat() (fi FileInfo, err error)

Stat返回描述文件f的FileInfo类型值。如果出错,错误底层类型是*PathError。这个方法也可以用于检查文件是否有问题,上面说到文件的信息是存储在FileInfo 接口中的,我们来看一下这个接口

FileInfo是一个接口,如下

// A FileInfo describes a file and is returned by Stat and Lstat.
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()  等价于 Mode().IsDir()
    Sys() interface{}   // underlying data source (can return nil)  底层数据来源(可以返回 nil)
}

该接口提供了一个sys函数,Sys() 底层数据的 C 语言 结构 statbuf 格式如下:

struct stat {
  dev_t    st_dev;    // 设备 ID
  ino_t    st_ino;    // 文件 i 节点号
  mode_t    st_mode;    // 位掩码,文件类型和文件权限
  nlink_t    st_nlink;    // 硬链接数
  uid_t    st_uid;    // 文件属主,用户 ID
  gid_t    st_gid;    // 文件属组,组 ID
  dev_t    st_rdev;    // 如果针对设备 i 节点,则此字段包含主、辅 ID
  off_t    st_size;    // 常规文件,则是文件字节数;符号链接,则是链接所指路径名的长度,字节为单位;对于共享内存对象,则是对象大小
  blksize_t    st_blsize;    // 分配给文件的总块数,块大小为 512 字节
  blkcnt_t    st_blocks;    // 实际分配给文件的磁盘块数量
  time_t    st_atime;        // 对文件上次访问时间
  time_t    st_mtime;        // 对文件上次修改时间
  time_t    st_ctime;        // 文件状态发生改变的上次时间
}
Go 中 syscal.Stat_t 与该结构对应。

如果我们要获取 FileInfo 接口没法直接返回的信息,比如想获取文件的上次访问时间,示例如下:

fileInfo, err := os.Stat("test.log")
if err != nil {
  log.Fatal(err)
}
sys := fileInfo.Sys()
stat := sys.(*syscall.Stat_t)
fmt.Println(time.Unix(stat.Atimespec.Unix()))

正常返回的是实现这个接口的结构体,也就是fileStat,如下

// A fileStat is the implementation of FileInfo returned by Stat and Lstat.
type fileStat struct {
    name    string
    size    int64
    mode    FileMode
    modTime time.Time
    sys     syscall.Stat_t
}

func (fs *fileStat) Size() int64        { return fs.size }
func (fs *fileStat) Mode() FileMode     { return fs.mode }
func (fs *fileStat) ModTime() time.Time { return fs.modTime }
func (fs *fileStat) Sys() interface{}   { return &fs.sys }

func sameFile(fs1, fs2 *fileStat) bool {
    return fs1.sys.Dev == fs2.sys.Dev && fs1.sys.Ino == fs2.sys.Ino
}

其中有一个syscall.Stat_t,源于syscall的结构体,这个结构体是需要区分系统的,不同的系统调用不一样,不然编译不通过,报错如下

registry/delete.go:49:27: stat.Ctimespec undefined (type *syscall.Stat_t has no field or method Ctimespec)

是因为在linux下结构体成名名是Ctim,在drawin下是Ctimespec,导致跨平台编译报错。

文件时间

通过包里的Chtimes函数可以显式改变文件的访问时间和修改时间。

func Chtimes(name string, atime time.Time, mtime time.Time) error

Chtimes 修改 name 指定的文件对象的访问时间和修改时间,类似 Unix 的 utime() 或 utimes() 函数。底层的文件系统可能会截断 / 舍入时间单位到更低的精确度。如果出错,会返回 *PathError 类型的错误。在 Unix 中,底层实现会调用 utimenstat(),它提供纳秒级别的精度

文件权限

系统调用 chown、lchown 和 fchown 可用来改变文件的属主和属组,Go 中os包中对应的函数或方法:

func Chown(name string, uid, gid int) error
func Lchown(name string, uid, gid int) error
func (f *File) Chown(uid, gid int) error

它们的区别和上文提到的 Stat 相关函数类似。

在文件相关操作报错时,可以通过 os.IsPermission 检查是否是权限的问题。

func IsPermission(err error) bool

返回一个布尔值说明该错误是否表示因权限不足要求被拒绝。ErrPermission 和一些系统调用错误会使它返回真。

另外,syscall.Access 可以获取文件的权限。这对应系统调用 access。

os.Chmod 和 os.File.Chmod 可以修改文件权限(包括 sticky 位),分别对应系统调用 chmod 和 fchmod。

目录

在 Unix 文件系统中,目录的存储方式类似于普通文件。目录和普通文件的区别有二:

  • 在其 i-node 条目中,会将目录标记为一种不同的文件类型。
  • 目录是经特殊组织而成的文件。本质上说就是一个表格,包含文件名和 i-node 标号

目录操作

创建

func Mkdir(name string, perm FileMode) error

Mkdir 使用指定的权限和名称创建一个目录。如果出错,会返回 *PathError 类型的错误。

  • name 参数指定了新目录的路径名,可以是相对路径,也可以是绝对路径。如果已经存在,则调用失败并返回 os.ErrExist 错误。
  • perm 参数指定了新目录的权限。对该位掩码值的指定方式和 os.OpenFile 相同,也可以直接赋予八进制数值。注意,perm 值还将于进程掩码相与(&)。如果 perm 中设置了 sticky 位,那么将对新目录设置该权限。

因为 Mkdir 所创建的只是路径名中的最后一部分,如果父目录不存在,创建会失败。os.MkdirAll 用于递归创建所有不存在的目录

func MkdirAll(path string, perm FileMode) error

MkdirAll使用指定的权限和名称创建一个目录,包括任何必要的上级目录,并返回nil,否则返回错误。权限位perm会应用在每一个被本函数创建的目录上。如果path指定了一个已经存在的目录,MkdirAll不做任何操作并返回nil。

删除

func Remove(name string) error

Remove 删除 name 指定的文件或目录。如果出错,会返回 *PathError 类型的错误。如果目录不为空,Remove 会返回失败。

func RemoveAll(path string) error

RemoveAll 删除 path 指定的文件,或目录及它包含的任何下级对象。它会尝试删除所有东西,除非遇到错误并返回。如果 path 指定的对象不存在,RemoveAll 会返回 nil 而不返回错误。

RemoveAll 的内部实现逻辑如下:

  • 调用 Remove 尝试进行删除,如果成功或返回 path 不存在,则直接返回 nil;
  • 调用 Lstat 获取 path 信息,以便判断是否是目录。注意,这里使用 Lstat,表示不对符号链接解引用;
  • 调用 Open 打开目录,递归读取目录中内容,执行删除操作。

func (f *File) Readdirnames(n int) (names []string, err error)

Readdirnames 读取目录 f 的内容,返回一个最多有 n 个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用未读取的内容的信息。

  • 如果 n>0,Readdirnames 函数会返回一个最多 n 个成员的切片。这时,如果 Readdirnames 返回一个空切片,它会返回一个非 nil 的错误说明原因。如果到达了目录 f 的结尾,返回值 err 会是 io.EOF。
  • 如果 n<=0,Readdirnames 函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果 Readdirnames 调用成功(读取所有内容直到结尾),它会返回该切片和 nil 的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。

Readdir 内部会调用 Readdirnames,将得到的 names 构造路径,通过 Lstat 构造出 []FileInfo。

func (f *File) Readdir(n int) (fi []FileInfo, err error)

连接

func Link(oldname, newname string) error

Link 创建一个名为 newname 指向 oldname 的硬链接。如果出错,会返回 *LinkError 类型的错误。

func Symlink(oldname, newname string) error

Symlink 创建一个名为 newname 指向 oldname 的符号链接。如果出错,会返回 *LinkError 类型的错误。

func Readlink(name string) (string, error)

Readlink 获取 name 指定的符号链接指向的文件的路径。如果出错,会返回 *PathError 类型的错误。

更改文件名

func Rename(oldpath, newpath string) error

Rename 修改一个文件的名字或移动一个文件。如果 newpath 已经存在,则替换它。注意,可能会有一些个操作系统特定的限制。

os/singal

类型

Signal是一个接口,所有的信号都实现了这个接口,可以直接传递,我们传递信号的时候,需要定义这个类型的channel来传递信号。

type Signal interface {
    String() string
    Signal() // to distinguish from other Stringers
}

syscall 包中定义了所有的信号常量,比如syscall.SIGINT,其实就是一个int的数字信号。

SIGINT    = Signal(0x2)
type Signal int

func (s Signal) Signal() {}

func (s Signal) String() string {
    if 0 <= s && int(s) < len(signals) {
        str := signals[s]
        if str != "" {
            return str
        }
    }
    return "signal " + itoa(int(s))
}

函数

Notify

singnal主要是用于信号的传递,一般程序中需要使用信号的时候使用。主要使用下面两个方法。

func Notify(c chan<- os.Signal, sig ...os.Signal)

Notify函数让signal包将输入信号转发到c。如果没有列出要传递的信号,会将所有输入信号传递到c;否则只传递列出的输入信号。

signal包不会为了向c发送信息而阻塞(就是说如果发送时c阻塞了,signal包会直接放弃):调用者应该保证c有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为1就足够了。

可以使用同一通道多次调用Notify:每一次都会扩展该通道接收的信号集。可以使用同一信号和不同通道多次调用Notify:每一个通道都会独立接收到该信号的一个拷贝。

实例

package main
import "fmt"
import "os"
import "os/signal"
import "syscall"
func main() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done <- true
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

stop

唯一从信号集去除信号的方法是调用Stop。

func Stop(c chan<- os.Signal)

Stop函数让signal包停止向c转发信号。它会取消之前使用c调用的所有Notify的效果。当Stop返回后,会保证c不再接收到任何信号。

os/exec

进程io

exec包用于执行外部命令。它包装了os.StartProcess函数以便更容易的修正输入和输出,使用管道连接I/O。主要用于创建一个子进程来执行相关的命令。创建子进程一定要wait,不能出现僵死进程。

调用脚本命令

在golang标准库中提供了两种方式可以用来启动进程调用脚本

第一种是在os库中的Process类型,Process类型包含一系列方法用来启动进程并对进程进行操作(参考: https://golang.org/pkg/os/#Process)

示例 使用Process执行脚本

package main

import (
    "fmt"
    "os"
)

func main() {
    shellPath := "/home/xx/test.sh"
    argv := make([]string, 1) 
    attr := new(os.ProcAttr)
    newProcess, err := os.StartProcess(shellPath, argv, attr)  //运行脚本
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("Process PID", newProcess.Pid)
    processState, err := newProcess.Wait() //等待命令执行完
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("processState PID:", processState.Pid())//获取PID
    fmt.Println("ProcessExit:", processState.Exited())//获取进程是否退出
}

第二种是在os/exec库种通过Cmd类型的各个函数实现对脚本的调用,实际上Cmd是对Process中各种方法的高层次封装(参考: https://golang.org/pkg/os/exec/)

1、LookPath

func LookPath(file string) (string, error)

在环境变量PATH指定的目录中搜索可执行文件,如file中有斜杠,则只在当前目录搜索。返回完整路径或者相对于当前目录的一个相对路径。

func main() {
    output, err := exec.LookPath("ls")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf(output)
}

output:

[ `go run test.go` | done: 616.254982ms ]
  /bin/ls

2、Cmd

Cmd代表一个正在准备或者在执行中的外部命令

type Cmd struct {
    // Path是将要执行的命令的路径。
    //
    // 该字段不能为空,如为相对路径会相对于Dir字段。
    Path string
    // Args保管命令的参数,包括命令名作为第一个参数;如果为空切片或者nil,相当于无参数命令。
    //
    // 典型用法下,Path和Args都应被Command函数设定。
    Args []string
    // Env指定进程的环境,如为nil,则是在当前进程的环境下执行。
    Env []string
    // Dir指定命令的工作目录。如为空字符串,会在调用者的进程当前目录下执行。
    Dir string
    // Stdin指定进程的标准输入,如为nil,进程会从空设备读取(os.DevNull)
    Stdin io.Reader
    // Stdout和Stderr指定进程的标准输出和标准错误输出。
    //
    // 如果任一个为nil,Run方法会将对应的文件描述符关联到空设备(os.DevNull)
    //
    // 如果两个字段相同,同一时间最多有一个线程可以写入。
    Stdout io.Writer
    Stderr io.Writer
    // ExtraFiles指定额外被新进程继承的已打开文件流,不包括标准输入、标准输出、标准错误输出。
    // 如果本字段非nil,entry i会变成文件描述符3+i。
    //
    // BUG: 在OS X 10.6系统中,子进程可能会继承不期望的文件描述符。
    // http://golang.org/issue/2603
    ExtraFiles []*os.File
    // SysProcAttr保管可选的、各操作系统特定的sys执行属性。
    // Run方法会将它作为os.ProcAttr的Sys字段传递给os.StartProcess函数。
    SysProcAttr *syscall.SysProcAttr
    // Process是底层的,只执行一次的进程。
    Process *os.Process
    // ProcessState包含一个已经存在的进程的信息,只有在调用Wait或Run后才可用。
    ProcessState *os.ProcessState
    // 内含隐藏或非导出字段
}

可以使用Command来创建cmd

func Command(name string, arg ...string) *Cmd

函数返回一个*Cmd,用于使用给出的参数执行name指定的程序。返回值只设定了Path和Args两个参数。

如果name不含路径分隔符,将使用LookPath获取完整路径;否则直接使用name。参数arg不应包含命令名

使用Run运行cmd命令

func (c *Cmd) Run() error

Run执行c包含的命令,并阻塞直到完成。如果命令成功执行,stdin、stdout、stderr的转交没有问题,并且返回状态码为0,方法的返回值为nil;如果命令没有执行或者执行失败,会返回错误;

使用Start和wait来运行命令

func (c *Cmd) Start() error

Start开始执行c包含的命令,但并不会等待该命令完成即返回。可以配合使用Wait方法来达到和Run一样的效果。wait方法会返回命令的返回状态码并在命令返回后释放相关的资源。

func (c *Cmd) Wait() error

Wait会阻塞直到该命令执行完成,该命令必须是被Start方法开始执行的。

通过Run的源码可以看出其实Run方法内部也是调用了Start和Wait方法。Run方法如下:

func (c *Cmd) Run() error {
    if err := c.Start(); err != nil {
        return err
    }
    return c.Wait()
}

实例

func main() {
    cmd := exec.Command("tr", "a-z", "A-Z")
    cmd.Stdin = strings.NewReader("abc def")
    var out bytes.Buffer
    cmd.Stdout = &out
    err := cmd.Run()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("GOGOGO: %q\n", out.String())
}

output:

[ `go run test.go` | done: 286.798242ms ]
  GOGOGO: "ABC DEF"

使用Output输出

func (c *Cmd) Output() ([]byte, error)

执行命令并返回标准输出的切片。

func (c *Cmd) CombinedOutput() ([]byte, error)

执行命令并返回标准输出和错误输出合并的切片.

实例

func main() {
    out, err := exec.Command("date").Output()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("The date is %s\n", out)
}

output:

[ `go run test.go` | done: 585.495467ms ]
  The date is Tue Aug  1 19:24:11 CST 2017

使用pipe

func (*Cmd) StdinPipe
func (c *Cmd) StdinPipe() (io.WriteCloser, error)

StdinPipe方法返回一个在命令Start后与命令标准输入关联的管道。Wait方法获知命令结束后会关闭这个管道。必要时调用者可以调用Close方法来强行关闭管道,例如命令在输入关闭后才会执行返回时需要显式关闭管道。

func (*Cmd) StdoutPipe
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)

StdoutPipe方法返回一个在命令Start后与命令标准输出关联的管道。Wait方法获知命令结束后会关闭这个管道,一般不需要显式的关闭该管道。但是在从管道读取完全部数据之前调用Wait是错误的;同样使用StdoutPipe方法时调用Run函数也是错误的。例子如下:

func (*Cmd) StderrPipe
func (c *Cmd) StderrPipe() (io.ReadCloser, error)

StderrPipe方法返回一个在命令Start后与命令标准错误输出关联的管道。Wait方法获知命令结束后会关闭这个管道,一般不需要显式的关闭该管道。但是在从管道读取完全部数据之前调用Wait是错误的;同样使用StderrPipe方法时调用Run函数也是错误的。请参照StdoutPipe的例子。

实例

func main() {
    cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }
    var person struct {
        Name string
        Age  int
    }
    json.NewDecoder(r)
    if err := json.NewDecoder(stdout).Decode(&person); err != nil {
        log.Fatal(err)
    }
    if err := cmd.Wait(); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d years old\n", person.Name, person.Age)
}

获取命令返回值

实际上脚本或命令执行完后,会将结果返回到ProcessState中的status去, 但是status不是export的,所以我们需要通过一些手段将脚本返回值从syscall.WaitStatus找出来

ProcessState定义

type ProcessState struct {
    pid    int                // The process's id.
    status syscall.WaitStatus // System-dependent status info.
    rusage *syscall.Rusage
}

对于上面使用Cmd的例子,可以在进程退出后可以通过以下语句获取到返回值

fmt.Println("Exit Code", command.ProcessState.Sys().(syscall.WaitStatus).ExitStatus())

使用Process方式的也可以通过对ProcessState通过相同的方式获取到返回结果。

os/user

os/user 模块的主要作用是通过用户名或者 id 从而获取系统用户的相关属性。

类型

User 结构体

type User struct {
    Uid      string
    Gid      string
    Username string
    Name     string
    HomeDir  string
}

User 代表一个用户账户:

  • Uid :用户的 ID
  • Gid :用户所属组的 ID,如果属于多个组,那么此 ID 为主组的 ID
  • Username :用户名
  • Name :属组名称,如果属于多个组,那么此名称为主组的名称
  • HomeDir :用户的宿主目录

方法

返回当前用户。

func Current() (*User, error)

通过用户名查找用户,如果没有找到这个用户那么将返回 UnknownUserError 错误类型。

func Lookup(username string) (*User, error)

通过用户 ID 查找用户,如果没有找到这个用户那么将返回 UnknownUserIdError 错误类型。

func LookupId(uid string) (*User, error)

实例

package main

import (
    "fmt"
    "os/user"
    "reflect"
)

func main() {
    fmt.Println("== 测试 Current 正常情况 ==")
    if u, err := user.Current(); err == nil {
        fmt.Println("用户ID: " + u.Uid)
        fmt.Println("主组ID: " + u.Gid)
        fmt.Println("用户名: " + u.Username)
        fmt.Println("主组名: " + u.Name)
        fmt.Println("家目录: " + u.HomeDir)
    }

    fmt.Println("== 测试 Lookup 正常情况 ==")
    if u, err := user.Lookup("root"); err == nil {
        fmt.Println("用户ID: " + u.Uid)
        fmt.Println("主组ID: " + u.Gid)
        fmt.Println("用户名: " + u.Username)
        fmt.Println("主组名: " + u.Name)
        fmt.Println("家目录: " + u.HomeDir)
    }
    fmt.Println("== 测试 Lookup 异常情况 ==")
    if _, err := user.Lookup("roo"); err == nil {
    } else {
        fmt.Println("错误信息: " + err.Error())
        fmt.Print("错误类型: ")
        fmt.Println(reflect.TypeOf(err))
    }

    fmt.Println("== 测试 LookupId 正常情况 ==")
    if u, err := user.LookupId("0"); err == nil {
        fmt.Println("用户ID: " + u.Uid)
        fmt.Println("主组ID: " + u.Gid)
        fmt.Println("用户名: " + u.Username)
        fmt.Println("主组名: " + u.Name)
        fmt.Println("家目录: " + u.HomeDir)
    }
    fmt.Println("== 测试 LookupId 异常情况 ==")
    if _, err := user.LookupId("10000"); err == nil {
    } else {
        fmt.Println("错误信息: " + err.Error())
        fmt.Print("错误类型: ")
        fmt.Println(reflect.TypeOf(err))
    }

}
输出结果如下:

== 测试 Current 正常情况 ==
用户ID: 0
主组ID: 0
用户名: root
主组名: root
家目录: /root
== 测试 Lookup 正常情况 ==
用户ID: 0
主组ID: 0
用户名: root
主组名: root
家目录: /root
== 测试 Lookup 异常情况 ==
错误信息: user: unknown user roo
错误类型: user.UnknownUserError
== 测试 LookupId 正常情况 ==
用户ID: 0
主组ID: 0
用户名: root
主组名: root
家目录: /root
== 测试 LookupId 异常情况 ==
错误信息: user: unknown userid 10000
错误类型: user.UnknownUserIdError