🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 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 } ``` 文件的第一行声明当前代码对应&mdash;"file"&mdash;包,然后导入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 $ ```