Go语言入门——进阶语法篇(四)

异常处理与文件

异常处理

error 处理

Go语言没有类似Java或Python那种try...catch...机制处理异常,Go的哲学是与众不同的,Go的设计者认为主流的异常处理机制是一种被过度滥用的技巧,而且存在很大的潜在危害,Go的异常处理(或者说是错误处理)是一种非常简单直观的方式。通常的,我们在写Java、Python之类的代码时,遇到可能存在的异常,直接用try括起来,使用catch捕获,然后就万事大吉了,当系统长时间的运行时,大大增加了不稳定性,所积累的问题可能在某一刻爆发。而Go者使用一种称为"恐慌的"机制,在有必要时,直接让系统宕机,让问题发生时立刻暴露出来,不必累积。很难说哪种设计更好,但Go语言确实简化了代码。

Go语言引入了一个错误处理的标准接口:error接口,并习惯性的默认将错误作为最后一个返回值,当然,如果有的话。如果我们要自定义错误类型,实现该接口即可

type error interface {
    Error() string
}

错误处理示例

type MyError struct {
	content   string
	errorCode int
}

// 让MyError实现error接口
func (this *MyError) Error() string {
	return fmt.Sprintf("%d:%s",this.errorCode,this.content)
}

func div(a, b int) (r int, err error) {
	var e *MyError = new(MyError)
	if b == 0 {
		e.errorCode = 101
		e.content = "除数不能为0"
		return 0, e
	}
	return a / b, nil
}

func main() {
	r, e := div(1, 0)
	if e != nil {
		// 错误不为空,打印错误
		fmt.Println(e)
		return
	}
	fmt.Println(r)
}

打印结果:

101:除数不能为0

自定义类型实现error接口可以提供更丰富的错误信息,但有时候我们希望快速的生成一个简单的错误,而不是写个结构体,那么Go还提供了一种快捷创建错误的方式,使用errors

package main

import (
	"fmt"
	"errors"
)

var errByZero = errors.New("除数不能为0")

func div(a, b int) (r int, err error) {
	if b == 0 {
		return 0, errByZero
	}
	return a / b, nil
}

注意,为了提升性能,errors.New方法不建议在函数中调用,错误的内容是不会变的,可以在函数外声明好需要的错误,就如同声明一些常量一样。该方法虽然简单,但是包含的错误信息有限,酌情使用。

panic 与 recover

panic词义为恐慌,recover则表示恢复。

仍以除数是0为例

func div(a, b int) int {
	if b == 0 {
		panic("crash:除数为0")
	}
	return a / b
}

func main() {
	r := div(1, 0)
	fmt.Println(r)
}

运行代码后,程序直接奔溃,并输出了调用信息

panic: crash:除数为0

goroutine 1 [running]:
main.div(...)
        C:/Users/ysk/Desktop/hello.go:12
main.main()
        C:/Users/ysk/Desktop/hello.go:18 +0x41
exit status 2

这样,当我在开发和调试时,出现问题,通过手动调用panic让程序崩溃,及时发现并解决问题,包括生成环境中的测试,而不是等到系统上线运行一段时候之后才发现问题。

有时候,我们可能很害怕奔溃,recover则可以在这种奔溃发生时,恢复程序,使得程序可以继续运行。简单说,panicrecover的组合,可以模拟实现Java中的try...catch机制,将异常捕获,而不是继续向上传递。但这并不是Go语言所推崇的用法。

func main() {
    // 加上一段代码,defer后面跟一个匿名函数,匿名函数中使用recover捕获到错误
	defer func(){
		err := recover()
		fmt.Printf("处理:%s\n",err)
	}()

	r := div(1, 0)
	fmt.Println(r)
}

打印结果:

处理:crash:除数为0

可以看到,使用recover处理后,程序不再奔溃了。

延迟处理

上面示例出现了一个关键字defer,该关键字就是用于延迟处理。我们上面说了Java中的trycatch,那怎么能没有finally呢。defer其实就相当于finally,在整个函数调用完后,最后执行一些关闭句柄的功能。Go中,defer除了关闭句柄,还可用于释放并发锁。

func main() {
	defer fmt.Println("这是defer调用")
	fmt.Println("Hello,world!")
	fmt.Println("Hello,go!")
}

打印结果:

Hello,world!
Hello,go!
这是defer调用

可以看到,defer语句写在最先,但是却在最后才被执行。同一个函数中是可以使用多个defer语句的,多个defer语句的执行顺序遵循栈结构特点,先进后出,最先的defer语句最后执行

func main() {
	defer fmt.Println("这是defer语句1")
	defer fmt.Println("这是defer语句2")
	
	fmt.Println("Hello,world!")
	defer fmt.Println("这是defer调用3")
	fmt.Println("Hello,go!")
	defer fmt.Println("这是defer调用4")
}

打印结果:

Hello,world!
Hello,go!
这是defer调用4
这是defer调用3
这是defer语句2
这是defer语句1

文件读写

带缓冲区

package main

import (
	"fmt"
	"os"
	"io"
	"bufio"  // 导入缓冲包
)

func main() {
	// 1. 创建文件
	file , err := os.Create("D:/test.txt")
    if err != nil{
        fmt.Println("创建文件失败")
        return
    }
    // 2. 关闭文件
    defer file.Close()

    // 3. 创建写入缓冲
	 w := bufio.NewWriter(file)
	// 4. 写入字符串
    len, err := w.WriteString("hello world")
    if err != nil {
      fmt.Println("写入失败")
    }else{
     fmt.Printf("写入了%d个字符\n",len)
    }  
    // 4. 刷新缓冲,写入硬盘
	w.Flush()
	
	// -------------------分割线---------------------

	// 1. 打开文件
    rfile, e := os.Open("D:/test.txt")
    if e != nil{
        fmt.Println("打开文件失败")
        return
    }
    // 2.关闭文件
	defer rfile.Close()
	
    // 3. 创建读取缓冲
	r := bufio.NewReader(rfile)
	
	// 4. 读取文件。使用带缓冲的方式读取文件,只能一行一行的读取
	// 该方式适用于高效的读写大文件
	for {
		str,err := r.ReadString('\n') 
		fmt.Println(str)

		if err == io.EOF {  // io.EOF表示文件的末尾
			break
		}
	}
}

小结

  1. 使用os.Create创建文件会覆盖掉已存在的文件
  2. 缓冲区默认大小为4096,可以使用NewReaderSizeNewWriterSize在创建缓冲时指定大小
  3. 如需以二进制方式读写文件,将WriteStringReadString换成WriteByte(c byte)ReadByte(),亦可使用Write(p []byte)Read(p []byte)方法。区别是WriteByteReadByte每次读写一个字节,WriteRead每次读写一个切片的字节。

使用 ioutil

在操作小文件时,可以不指定缓冲区,那么就可以使用一种更简单的方式读写文件。

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	// 1. 字符串转字节切片
	buf := []byte("golang write string")
	// 2. 将字节切片直接写入文件,文件不存在则创建,存在则覆盖
	err := ioutil.WriteFile("D:/io_test.txt", buf, 0666)
	if err != nil {
		fmt.Println("写入失败")
	} else {
		fmt.Println("写入成功")
	}

	// -------------------分割线---------------------

	// 1. 读取指定文件,返回字节切片
	data, e := ioutil.ReadFile("D:/io_test.txt")
	if e != nil {
		fmt.Println(e)
	} else {
		// 2. 将字节切片转字符串输出
		fmt.Println(string(data))
	}
}

小结

  1. 使用ioutil不需要手动打开和关闭文件,打开和关闭操作已被封装了
  2. 使用ioutil.WriteFile仍然存在覆盖已有文件的问题,如需对文件进行追加操作,应使用其他方式
  3. WriteFileReadFile是以字节的方式操作文件,如需处理文本文件,应手动转换字节与字符串

文件追加

很多时候我们不希望新文件覆盖旧文件,而是在旧文件中继续添加内容。这时候必须使用指定模式的方式来打开文件。

package main

import (
	"fmt"
	"bufio"
	"os"
)

func main() {
	// 1. 打开文件,指定文件操作模式: 读写追加
	file,err := os.OpenFile("D:/test.txt",os.O_RDWR|os.O_APPEND,0666)
	if err != nil{
        fmt.Println("打开文件失败")
        return
	}
	
	// 2. 函数执行结束关闭文件
	defer file.Close()

	// 3. 创建写缓冲
	writer := bufio.NewWriter(file)
	writer.WriteString("text content")

	// 4. 刷新缓冲
	writer.Flush()
}

常用组合

模式组合 说明
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 可读可写模式
os.O_WRONLY|os.O_CREATE 写|创建
os.O_WRONLY|os.O_TRUNC 写|覆盖
O_WRONLY|os.O_APPEND 写|追加
os.O_RDWR|os.O_APPEND 读写|追加

小结

  • os.OpenFile函数的最后一个参数表示Unix系统中的文件权限,在Windows系统上被忽略。

文件的其他操作

获取文件信息

package main

import (
	"fmt"
	"os"
)

func main() {
	// 获取文件信息
	fileInfo, err := os.Stat("D:/test.txt")
	if err != nil{
        fmt.Println("打开文件失败")
        return
	}
	fmt.Println("文件名:", fileInfo.Name())
	fmt.Println("文件大小:", fileInfo.Size())
	fmt.Println("文件权限:", fileInfo.Mode())
	fmt.Println("最后修改时间:", fileInfo.ModTime())
	fmt.Println("是否是文件夹:", fileInfo.IsDir())
	fmt.Printf("系统信息:%+v\n", fileInfo.Sys())
}

判断文件是否存在

package main

import (
	"fmt"
	"os"
)

// 定义一个函数,判断文件是否存在
func exists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        return true, nil
    }
    if os.IsNotExist(err) {
        return false, nil
    }
    return false, err
}

func main() {
	if b,_ := exists("D:/test.txt");b {
		fmt.Println("文件存在")
	}else{
		fmt.Println("文件不存在")
	}
}

文件拷贝

package main

import (
	"fmt"
	"os"
	"io"
	"bufio"
)

func main() {
	var dst string = "D:/files/test.txt" // 目标路径
    var src string = "D:/test.txt"       // 源路径

    srcFile ,err := os.Open(src)
    if err != nil {
        fmt.Println("打开失败:",err)
        return 
    }
    defer srcFile.Close()
    reader := bufio.NewReader(srcFile)


    dstFile,err := os.OpenFile(dst,os.O_WRONLY|os.O_CREATE,0666)
    if err != nil {
        fmt.Println("打开失败:",err)
        return 
    }
    defer dstFile.Close()

	writer := bufio.NewWriter(dstFile)
	
	// 使用Coyp函数完成文件拷贝
    if _, err = io.Copy(writer, reader); err != nil {
        fmt.Println("拷贝失败:",err)
        return
    }
    fmt.Println("拷贝成功!")
}

移动、删除和重命名

package main

import (
	"fmt"
	"os"
)

func main() {
	src ,dst := "D:/test.txt","D:/workspace/test.txt"

	// 文件移动(Rename既可重命名也可移动文件)
	err := os.Rename(src, dst)
    if err != nil {
        fmt.Println("移动失败:",err)
        return 
    }
	// 文件删除
	err = os.Remove("D:/io_test.txt")
	if err != nil {
        fmt.Println("删除失败:",err)
        return 
    }
    // 文件重命名
	oldName, newName := "D:/logcat.txt", "D:/log.txt"
    err = os.Rename(oldName, newName)
    if err != nil {
        fmt.Println("重命名失败:",err)
        return 
    }
}

欢迎关注我的公众号:编程之路从0到1

编程之路从0到1

猜你喜欢

转载自blog.csdn.net/yingshukun/article/details/100190027