专栏首页编程之路的专栏Go语言入门——进阶语法篇(四)

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

  • 异常处理与文件
    • 异常处理
      • error 处理
      • panic 与 recover
      • 延迟处理
    • 文件读写
      • 带缓冲区
      • 使用 ioutil
      • 文件追加
      • 文件的其他操作
        • 获取文件信息
        • 判断文件是否存在
        • 文件拷贝
        • 移动、删除和重命名

异常处理与文件

异常处理

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()
}

常用组合

只读模式

小结

  • 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(artofprogram)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spring【AOP模块】就这么简单

    到目前为止,已经简单学习了Spring的Core模块….于是我们就开启了Spring的AOP模块了…在讲解AOP模块之前,首先我们来讲解一下cglib代理、以及...

    乔戈里
  • 现代Web开发需要学习的15大技术

    将近4年前,我写了一篇名为《Future of Web and Mobile: HTML5, CSS3 and Javascript》的博客文章,其中我提到了J...

    猿哥
  • 【Leetcode】139.拆分词句

    给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

    Leetcode名企之路
  • java练习本(2019-08-14)

    “Those that go searching for love, only manifest their own lovelessness. And the...

    微笑的小小刀
  • 190. 颠倒二进制位

    请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无...

    lucifer210
  • Jetbrains好用的插件(经验总结)

    笔者使用过idea,phpstorm,webstorm,pycharm和goland开发过项目,不得不说,Jetbrains的编辑器每一款都挺好用的,而且快捷键...

    全菜工程师小辉
  • Spring的设计模式快速入门干货

    设计模式是一套被反复使用的、多数人知晓的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

    全菜工程师小辉
  • Java内存映射,上G大文件轻松处理

    稍微解释一下虚拟内存(很明显,不是物理内存),它是计算机系统内存管理的一种技术。像施了妖法一样使得应用程序认为它拥有连续的可用的内存,实际上呢,它通常是被分隔成...

    沉默王二
  • Java Serializable:明明就一个空的接口嘛

    对于 Java 的序列化,我一直停留在最浅显的认知上——把那个要序列化的类实现 Serializbale 接口就可以了。我不愿意做更深入的研究,因为会用就行了嘛...

    沉默王二
  • 反射

    当程序要使用某个类时,如果该类还未被加载到内存中,系统会通过加载,连接,初始化三步来实现对这个类进行初始化

    木瓜煲鸡脚

扫码关注云+社区

领取腾讯云代金券