第七天:PHP开发者快速掌握Go错误处理与文件操作 | 从PHP异常/文件函数到Go实战
作为PHP开发者,你熟悉用try/catch处理异常、file_get_contents/file_put_contents读写文件、mkdir创建目录。Go没有“异常(Exception)”的概念,而是通过error接口返回错误;文件操作依赖os、bufio等标准库,配合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 error | PHP 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_contents、fwrite、mkdir等函数。
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的核心知识点:
Go用
error接口返回错误(替代PHP Exception),通过判断返回值是否为nil处理错误;defer关键字延迟执行函数,保证资源释放(替代PHP finally);文件读取:
os.ReadFile(一次性)、bufio.Scanner(逐行),对标file_get_contents/fgets;文件写入:
os.WriteFile(覆盖)、os.OpenFile+O_APPEND(追加),对标file_put_contents;os.MkdirAll创建多级目录,对标mkdir($path, 0755, true)。
明天我们将学习Go的并发编程(Goroutine与Channel),对比PHP的多进程/多线程,详解Go原生并发的核心逻辑,继续用PHP思维快速上手Go的核心优势。
