第七天:PHP开发者快速掌握Go错误处理与文件操作 | 从PHP异常/文件函数到Go实战

yvsm5个月前Go语言10480

作为PHP开发者,你熟悉用try/catch处理异常、file_get_contents/file_put_contents读写文件、mkdir创建目录。Go没有“异常(Exception)”的概念,而是通过error接口返回错误;文件操作依赖osbufio等标准库,配合defer关键字保证资源释放,逻辑与PHP一致但语法更严谨。今天我们以PHP的错误/文件操作逻辑为参照,快速掌握Go的错误处理和文件操作核心。

一、Go错误处理(对标PHP异常处理)

PHP用Exception表示异常,通过try/catch/finally捕获和处理;Go用内置的error接口表示错误,通过“返回值”传递错误,配合defer实现finally的资源释放效果。

1. 基础error接口(对标PHP Exception)

Go的error是一个极简接口(仅包含Error() string方法),所有错误类型都实现了该接口,对标PHP的基础Exception类。

特性Go errorPHP Exception
定义方式内置接口,无需声明内置类,可继承扩展
传递方式作为函数返回值(多返回值)通过throw抛出,try/catch捕获
资源释放defer关键字finally代码块

基础示例:Go返回错误 vs PHP抛出异常

// Go:函数返回error(判断是否为nil,对标PHP catch)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        // 返回错误(errors.New创建基础error)
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil // 无错误返回nil
}

func main() {
    res, err := divide(10, 0)
    // 判断错误是否存在(对标PHP if (e instanceof Exception))
    if err != nil {
        fmt.Println("错误:", err) // 输出:错误:除数不能为0
        return
    }
    fmt.Println("结果:", res)
}

PHP对应写法:

// PHP:抛出并捕获异常
function divide($a, $b) {
    if ($b == 0) {
        throw new Exception("除数不能为0");
    }
    return $a / $b;
}

// try/catch捕获异常
try {
    $res = divide(10, 0);
    echo "结果:" . $res . "\n";
} catch (Exception $e) {
    echo "错误:" . $e->getMessage() . "\n"; // 输出:错误:除数不能为0
}

2. 自定义错误(对标PHP自定义异常)

PHP通过继承Exception创建自定义异常;Go通过实现error接口,或用fmt.Errorf创建带格式化信息的错误,实现自定义错误。

// Go:自定义错误(方式1:fmt.Errorf格式化)
func checkAge(age int) error {
    if age < 0 || age > 150 {
        // 格式化错误信息,对标PHP自定义异常的getMessage
        return fmt.Errorf("年龄%d非法,需在0-150之间", age)
    }
    return nil
}

// Go:自定义错误(方式2:实现error接口)
type FileError struct {
    Path string
    Msg  string
}

// 实现error接口的Error方法
func (e *FileError) Error() string {
    return fmt.Sprintf("文件错误[%s]:%s", e.Path, e.Msg)
}

func main() {
    // 测试fmt.Errorf
    if err := checkAge(-5); err != nil {
        fmt.Println(err) // 输出:年龄-5非法,需在0-150之间
    }

    // 测试自定义error接口
    err := &FileError{
        Path: "test.txt",
        Msg:  "文件不存在",
    }
    fmt.Println(err) // 输出:文件错误[test.txt]:文件不存在
}

PHP对应写法:

// PHP:自定义异常类
class FileError extends Exception {
    private $path;

    public function __construct($path, $msg) {
        $this->path = $path;
        parent::__construct($msg);
    }

    // 自定义方法
    public function getFilePath() {
        return $this->path;
    }
}

// 自定义异常使用
function checkAge($age) {
    if ($age < 0 || $age > 150) {
        throw new Exception("年龄{$age}非法,需在0-150之间");
    }
}

try {
    checkAge(-5);
} catch (Exception $e) {
    echo $e->getMessage() . "\n"; // 输出:年龄-5非法,需在0-150之间
}

try {
    throw new FileError("test.txt", "文件不存在");
} catch (FileError $e) {
    echo "文件错误[{$e->getFilePath()}]:{$e->getMessage()}\n";
}

3. defer关键字(对标PHP finally)

Go的defer用于延迟执行函数(在当前函数执行完毕后执行),常用来释放资源(如关闭文件、数据库连接),对标PHP的finally代码块。

// Go:defer释放文件资源(对标PHP finally关闭文件)
func readFile(path string) error {
    // 打开文件
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    // 延迟关闭文件(无论函数是否出错,都会执行)
    defer file.Close()

    // 读取文件内容(省略)
    fmt.Println("文件打开成功,后续执行读取逻辑")
    return nil
}

func main() {
    if err := readFile("test.txt"); err != nil {
        fmt.Println("错误:", err)
    }
}

PHP对应写法:

// PHP:finally关闭文件
function readFile($path) {
    $file = fopen($path, "r");
    try {
        if (!$file) {
            throw new Exception("文件打开失败");
        }
        echo "文件打开成功,后续执行读取逻辑\n";
    } catch (Exception $e) {
        echo "错误:" . $e->getMessage() . "\n";
    } finally {
        // 无论是否出错,关闭文件
        if ($file) {
            fclose($file);
        }
    }
}

readFile("test.txt");

💡 核心知识点:
       1. defer按“后进先出”执行(多个defer时,最后声明的先执行);
       2. defer可捕获函数返回值(用于修改返回结果);
       3. Go的panic/recover类似PHP的“致命错误”,仅用于不可恢复的错误(极少使用)。

二、Go文件操作(对标PHP文件函数)

Go的文件操作主要依赖os(基础文件操作)、bufio(缓冲读写)、io/ioutil(简化读写,Go1.16后整合到os),对标PHP的file_get_contentsfwritemkdir等函数。

1. 文件读取(对标PHP file_get_contents/fgets)

Go读取文件有两种核心方式:os.ReadFile(一次性读取,对标file_get_contents)、bufio.Scanner(逐行读取,对标fgets)。

// Go:一次性读取文件(对标file_get_contents)
func readFileAll(path string) {
    // 一次性读取文件内容,返回[]byte和error
    content, err := os.ReadFile(path)
    if err != nil {
        fmt.Println("读取错误:", err)
        return
    }
    // 转换为字符串输出
    fmt.Println("文件内容:", string(content))
}

// Go:逐行读取文件(对标fgets)
func readFileLineByLine(path string) {
    file, err := os.Open(path)
    if err != nil {
        fmt.Println("打开错误:", err)
        return
    }
    defer file.Close()

    // 创建缓冲扫描器,逐行读取
    scanner := bufio.NewScanner(file)
    lineNum := 1
    for scanner.Scan() {
        fmt.Printf("第%d行:%s\n", lineNum, scanner.Text())
        lineNum++
    }

    // 检查扫描过程中的错误
    if err := scanner.Err(); err != nil {
        fmt.Println("扫描错误:", err)
    }
}

PHP对应写法:

// PHP:一次性读取文件(file_get_contents)
function readFileAll($path) {
    $content = file_get_contents($path);
    if ($content === false) {
        echo "读取错误:" . error_get_last()['message'] . "\n";
        return;
    }
    echo "文件内容:" . $content . "\n";
}

// PHP:逐行读取文件(fgets)
function readFileLineByLine($path) {
    $file = fopen($path, "r");
    if (!$file) {
        echo "打开错误:" . error_get_last()['message'] . "\n";
        return;
    }

    $lineNum = 1;
    while (($line = fgets($file)) !== false) {
        echo "第{$lineNum}行:" . $line . "\n";
        $lineNum++;
    }

    fclose($file);
}

2. 文件写入/追加(对标PHP file_put_contents)

Go用os.WriteFile写入文件(对标file_put_contents),用os.OpenFile指定os.O_APPEND实现追加写入。

// Go:写入文件(覆盖模式,对标file_put_contents($path, $content))
func writeFile(path string, content string) {
    // 参数:路径、内容([]byte)、文件权限(0644对应PHP的默认权限)
    err := os.WriteFile(path, []byte(content), 0644)
    if err != nil {
        fmt.Println("写入错误:", err)
        return
    }
    fmt.Println("写入成功")
}

// Go:追加写入(对标file_put_contents($path, $content, FILE_APPEND))
func appendFile(path string, content string) {
    // 打开文件:O_WRONLY(只写)+O_CREATE(不存在则创建)+O_APPEND(追加)
    file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        fmt.Println("打开错误:", err)
        return
    }
    defer file.Close()

    // 写入内容
    _, err = file.WriteString(content)
    if err != nil {
        fmt.Println("追加错误:", err)
        return
    }
    fmt.Println("追加成功")
}

PHP对应写法:

// PHP:写入文件(覆盖模式)
function writeFile($path, $content) {
    $res = file_put_contents($path, $content);
    if ($res === false) {
        echo "写入错误:" . error_get_last()['message'] . "\n";
        return;
    }
    echo "写入成功\n";
}

// PHP:追加写入
function appendFile($path, $content) {
    $res = file_put_contents($path, $content, FILE_APPEND);
    if ($res === false) {
        echo "追加错误:" . error_get_last()['message'] . "\n";
        return;
    }
    echo "追加成功\n";
}

3. 目录操作(对标PHP mkdir)

Go用os.Mkdir创建单级目录、os.MkdirAll创建多级目录,对标PHP的mkdir(单级)和mkdir($path, 0755, true)(多级)。

// Go:创建目录
func createDir() {
    // 创建单级目录(对标mkdir("test", 0755))
    err := os.Mkdir("test", 0755)
    if err != nil && !os.IsExist(err) { // 判断是否已存在
        fmt.Println("创建单级目录错误:", err)
    }

    // 创建多级目录(对标mkdir("a/b/c", 0755, true))
    err = os.MkdirAll("a/b/c", 0755)
    if err != nil {
        fmt.Println("创建多级目录错误:", err)
        return
    }
    fmt.Println("目录创建成功")
}

PHP对应写法:

// PHP:创建目录
function createDir() {
    // 创建单级目录
    if (!mkdir("test", 0755) && !is_dir("test")) {
        echo "创建单级目录错误:" . error_get_last()['message'] . "\n";
    }

    // 创建多级目录
    if (!mkdir("a/b/c", 0755, true) && !is_dir("a/b/c")) {
        echo "创建多级目录错误:" . error_get_last()['message'] . "\n";
        return;
    }
    echo "目录创建成功\n";
}

⚠️ 核心差异:
       1. Go的文件权限参数(如0644、0755)是八进制数,需加前导0;PHP的权限参数直接写数字(如0755);
       2. Go判断文件/目录是否存在需用os.IsExist(err);PHP用file_exists/is_dir
       3. Go的os.ReadFile/os.WriteFile会自动打开/关闭文件,无需手动操作。

三、实战案例:PHP vs Go 读写文件并处理错误

需求:读取source.txt的内容,筛选出包含“Go”的行,写入target.txt,处理所有可能的错误(文件不存在、权限不足等)。

Go写法(error+defer+bufio)

package main

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

func filterAndWrite(sourcePath, targetPath string) error {
    // 打开源文件
    srcFile, err := os.Open(sourcePath)
    if err != nil {
        return fmt.Errorf("打开源文件失败:%w", err) // 包装错误信息
    }
    defer srcFile.Close()

    // 创建/打开目标文件(覆盖模式)
    dstFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    if err != nil {
        return fmt.Errorf("打开目标文件失败:%w", err)
    }
    defer dstFile.Close()

    // 逐行读取源文件
    scanner := bufio.NewScanner(srcFile)
    writer := bufio.NewWriter(dstFile) // 缓冲写入,提升性能
    defer writer.Flush() // 确保缓冲内容写入文件

    for scanner.Scan() {
        line := scanner.Text()
        if strings.Contains(line, "Go") {
            // 写入目标文件,加换行符
            _, err := writer.WriteString(line + "\n")
            if err != nil {
                return fmt.Errorf("写入行失败:%w", err)
            }
        }
    }

    // 检查扫描错误
    if err := scanner.Err(); err != nil {
        return fmt.Errorf("读取源文件失败:%w", err)
    }

    return nil
}

func main() {
    if err := filterAndWrite("source.txt", "target.txt"); err != nil {
        fmt.Println("处理失败:", err)
        return
    }
    fmt.Println("处理成功")
}

PHP写法

// PHP:读写文件并处理错误
function filterAndWrite($sourcePath, $targetPath) {
    // 检查源文件是否存在
    if (!file_exists($sourcePath)) {
        throw new Exception("源文件不存在:{$sourcePath}");
    }

    // 打开源文件
    $srcFile = fopen($sourcePath, "r");
    if (!$srcFile) {
        throw new Exception("打开源文件失败:" . error_get_last()['message']);
    }

    // 打开目标文件(覆盖模式)
    $dstFile = fopen($targetPath, "w");
    if (!$dstFile) {
        fclose($srcFile);
        throw new Exception("打开目标文件失败:" . error_get_last()['message']);
    }

    // 逐行读取并筛选
    while (($line = fgets($srcFile)) !== false) {
        if (str_contains($line, "Go")) {
            fwrite($dstFile, $line);
        }
    }

    // 关闭文件
    fclose($srcFile);
    fclose($dstFile);

    // 检查读取错误
    if (!feof($srcFile)) {
        throw new Exception("读取源文件失败:" . error_get_last()['message']);
    }
}

// 调用并处理异常
try {
    filterAndWrite("source.txt", "target.txt");
    echo "处理成功\n";
} catch (Exception $e) {
    echo "处理失败:" . $e->getMessage() . "\n";
}

五、今日小结

今天我们以PHP的异常/文件操作逻辑为参照,掌握了Go的核心知识点:

  1. Go用error接口返回错误(替代PHP Exception),通过判断返回值是否为nil处理错误;

  2. defer关键字延迟执行函数,保证资源释放(替代PHP finally);

  3. 文件读取:os.ReadFile(一次性)、bufio.Scanner(逐行),对标file_get_contents/fgets

  4. 文件写入:os.WriteFile(覆盖)、os.OpenFile+O_APPEND(追加),对标file_put_contents

  5. os.MkdirAll创建多级目录,对标mkdir($path, 0755, true)

明天我们将学习Go的并发编程(Goroutine与Channel),对比PHP的多进程/多线程,详解Go原生并发的核心逻辑,继续用PHP思维快速上手Go的核心优势。

相关文章

第三天:PHP 开发者快速掌握 Go 运算符与流程控制

作为PHP开发者,你早已熟悉if/else、for、foreach等流程控制语法,以及+/-/*/、==/===等运算符。Go的运算符和流程控制语法与PHP高度相似,但也有不少“细节差异”——比如Go...

第八天:PHP开发者快速掌握Go并发编程 | Goroutine与Channel从入门到实战

作为PHP开发者,你知道实现并发需要依赖pcntl扩展创建多进程、pthreads扩展实现多线程,或借助Swoole/Workerman框架——步骤繁琐且资源消耗高。而Go内置原生并发支持:Gorou...

第五天:PHP开发者快速掌握Go数组、切片与Map | 从PHP数组到Go复合类型的转换

作为PHP开发者,你早已习惯了“万能数组”——既可以当索引数组用,也可以当关联数组用,长度动态变化、元素类型不限。而Go没有“万能数组”,而是将复合数据类型拆分为数组(array)、切...

第九天:PHP开发者快速掌握Go项目实战 | 模块化、依赖管理与Web服务开发

作为PHP开发者,你熟悉用命名空间组织代码、Composer管理依赖、PHP-FPM+原生/框架(如Laravel/ThinkPHP)开发Web服务。Go从1.11版本开始引入Go Modules实现...

第一天:从PHP到Golang,零基础入门的环境搭建与第一个程序

作为一名PHP开发者,当你决定接触Golang(简称Go)时,首先会好奇它和熟悉的PHP有哪些差异,以及如何快速迈出第一步。今天我们就从环境搭建和第一个Go程序入手,用PHP开发者熟悉的思维来解锁Go...

第六天:PHP开发者快速掌握Go结构体与接口 | 从PHP类/接口到Go面向对象

作为PHP开发者,你熟悉用class定义类、interface定义接口,通过实例化对象实现面向对象编程。Go没有“类(class)”的概念,但通过结构体(struct)实现类的属性封装,通过方法(me...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。