4.9. I/O包

接下来我们使用open/close/read/write等基本的系统调用实现一个用于文件IO的包。 让我们从文件file.go开始:

  05    package file

  07    import (
  08        "os"
  09        "syscall"
  10    )

  12    type File struct {
  13        fd   int    // file descriptor number
  14        name string // file name at Open time
  15    }

文件的第一行声明当前代码对应—"file"—包,然后导入os和syscall两个包。 包os封装了不同操作系统底层的实现,例如将文件抽象成相同的类型。我们将在系统接口基础 上封装一个基本的文件IO接口。

另外还有其他一些比较底层的syscall包,它提供一些底层的系统调用(system's calls)。

接下来是一个类型(type)定义:用"type"这个关键字来声明一个类。在这个例子里数据结构(data structure) 名为"File"。为了让这事变的有趣些,我们的File包含了一个这个文件的名字(name)用来描述这个文件。

因为结构体名字"File"的首字母是大写,所以这个类型包(package)可以被外部访问。在GO中访问规则的处理 是非常简单的:如果顶极类型名字首字母(包括:function, method, constant or variable, or of a structure field or method)是大写,那么引用了这个包(package)的使用者就可以访问到它。不然 名称和被命名的东西将只能有package内部看到。这是一个要严格遵循的规则,因为这个访问规则是由 编译器(compiler)强制规范的。在GO中,一组公开可见的名称是"exported"。

在这个File例子中,所有的字段(fields)都是小写所以从包外部是不能访问的,不过我们在下面将会一个 一个对外访问的出口(exported) —— 一个以大写字母开头的方法。

首先是一个创建File结构体的函数:

  17    func newFile(fd int, name string) *File {
  18        if fd < 0 {
  19            return nil
  20        }
  21        return &File{fd, name}
  22    }

这将返回一个指向新File结构体的指针,结构体存有文件描述符和文件名。这段代码使用了GO的复合变量(composite literal)的概念,和创建内建的maps和arrays类型变量一样。要创建在堆(heap-allocated)中创建一个新的 对象,我们可以这样写:

          n := new(File);
          n.fd = fd;
          n.name = name;
          return n

如果结构比较简单的话,我们可以直接在返回结构体变量地址的时候初始化成员字段,如前面例子的 21行代码所示。

我们可以用前面的函数(newFile)构造一些File类型的变量,返回File:

  24    var (
  25        Stdin  = newFile(0, "/dev/stdin")
  26        Stdout = newFile(1, "/dev/stdout")
  27        Stderr = newFile(2, "/dev/stderr")
  28    )

这里的newFile是内部函数,真正包外部可以访问的函数是Open:

  30    func Open(name string, mode int, perm uint32) (file *File, err os.Error) {
  31        r, e := syscall.Open(name, mode, perm)
  32        if e != 0 {
  33            err = os.Errno(e)
  34        }
  35        return newFile(r, name), err
  36    }

在这几行里出现了一些新的东西。首先,函数Open返回多个值(multi-value):一个File指针和一个error( 等一下会介绍errors)》我们用括号来表来声明返回多个变量值(multi-value),语法上它看 起来像第二个参数列表。syscall.Open系统调用同样也是返回多个值multi-value。接着我们能在31行 创建了r和e两个变量用于保存syscall.Open的返回值。函数最终也是返回2个值,分别为File指针和一个error。 如果syscall.Open打开失败,文件描述r将会是个负值,newFile将会返回nil。

关于错误:os包包含了一些常见的错误类型。在用户自己的代码中也尽量使用这些通用的错误。 在Open函数中,我们用os.Error函数将Unix的整数错误代码转换为go语言的错误类型。

现在我们可以创建Files,我们为它定义了一些常用的方法(methods)。要给一个类型定义一个方法(method), 需要在函数名前增加一个用于访问当前类型的变量。这些是为*File类型创建的一些方法:

  38    func (file *File) Close() os.Error {
  39        if file == nil {
  40            return os.EINVAL
  41        }
  42        e := syscall.Close(file.fd)
  43        file.fd = -1 // so it can't be closed again
  44        if e != 0 {
  45            return os.Errno(e)
  46        }
  47        return nil
  48    }

  50    func (file *File) Read(b []byte) (ret int, err os.Error) {
  51        if file == nil {
  52            return -1, os.EINVAL
  53        }
  54        r, e := syscall.Read(file.fd, b)
  55        if e != 0 {
  56            err = os.Errno(e)
  57        }
  58        return int(r), err
  59    }

  61    func (file *File) Write(b []byte) (ret int, err os.Error) {
  62        if file == nil {
  63            return -1, os.EINVAL
  64        }
  65        r, e := syscall.Write(file.fd, b)
  66        if e != 0 {
  67            err = os.Errno(e)
  68        }
  69        return int(r), err
  70    }

  72    func (file *File) String() string {
  73        return file.name
  74    }

这些并没有隐含的this指针(参考C++类),而且类型的方法(methods)也不是定义在struct内部——struct结构 只声明数据成员(data members)。事实上,我们可以给任意数据类型定义方法,例如:整数(integer),数组(array) 等。后面我们会有一个给数组定义方法的例子。

String这个方法之所以会被调用是为了更好的打印信息,我们稍后会详细说明。

方法(methods)使用os.EINVAL来表示(os.Error的版本)Unix错误代码EINVAL。 在os包中针对标准的error变量定义各种错误常量。

现在我们可以使用我们自己创建的包(package)了:

  05    package main

  07    import (
  08        "./file"
  09        "fmt"
  10        "os"
  11    )

  13    func main() {
  14        hello := []byte("hello, world\n")
  15        file.Stdout.Write(hello)
  16        file, err := file.Open("/does/not/exist",  0,  0)
  17        if file == nil {
  18            fmt.Printf("can't open file; err=%s\n",  err.String())
  19            os.Exit(1)
  20        }
  21    }

这个"./"在导入(import)"./file"时告诉编译器(compiler)使用我们自己的package,而不是在 默认的package路径中找。

最后,我们来执行这个程序:

      $ 6g file.go                       # compile file package
      $ 6g helloworld3.go                # compile main package
      $ 6l -o helloworld3 helloworld3.6  # link - no need to mention "file"
      $ helloworld3
      hello, world
      can't open file; err=No such file or directory
      $