سرمایه گذاری متفاوت در سال نو 🍎🌱 ۳۵٪ تخفیف نوروزی ➕ حضور رایگان در مسترمایند نخبگان صنعت نرم‌افزار 💻✅
۰ ثانیه
۰ دقیقه
۰ ساعت
۳ دانشجوی سون‌لرن
نحوه ی call شدن متد Error
جامعه گولنگ ایجاد شده در ۰۹ تیر ۱۴۰۲

سلام وقت بخیر

من یه سوالی داشتم در رابطه با نحوه ی call شدن متد Error

این کد را در نظیر بگیرید

package main
import (
    "errors"
    "fmt"
    "io"
    "os"
)
type IOError struct {
    FileName string
    Message  string
    Err      error
}
func (error *IOError) Unwrap() error {
    return error.Err
}
func (error IOError) Error() string {
    return fmt.Sprintf("IO error occurred: FileName: %s Message: %s Detail: %s", error.FileName, error.Message, error.Err.Error())
}
func CopyFile(srcName, dstName string) error {
    src, err := os.Open(srcName)
    if err != nil {
        return &IOError{FileName: srcName, Message: "during copy file could not open source file", Err: err}
    }
    //defer src.Close()
    dst, err := os.Create(dstName)
    if err != nil {
        return &IOError{FileName: srcName, Message: "during copy file could not create destination file", Err: err}
    }
    dst.Close()
    _, err = io.Copy(dst, src)
    fmt.Printf("**************%s***************\\n", err)
    if err != nil {
        return &IOError{FileName: srcName, Message: "during copy file could not copy", Err: err}
    }
    return nil
}
func main() {
    err := CopyFile("src.txt", "dst.txt")
    fmt.Printf("%s\\n", err)
    fmt.Printf("Unwrap Error: %s", errors.Unwrap(err))
    return
    if err != nil {
        fmt.Printf("Error: %s\\n", err)
    }
}

وقتی خطای err را در main پرینت میکنیم میره متد Error را که خودمان rewrite کردیم call میکنه اما در فانکشن CopyFile وقتی مقدار err را printf میکنم سراغ متد Error نمیره

این قسمت برام مبهم هست اگر امکانش هست نحوه ی call شدن متد Error را trace کنید تا بیشتر متوجه بشیم ؟

//output 
**************write dst.txt: file already closed***************
IO error occurred: FileName: src.txt Message: during copy file could not copy Detail: write dst.txt: file already closed
Unwrap Error: write dst.txt: file already closed

مشکلی که مطرح کردید مربوط به نحوه فراخوانی متد `Error()` هستش و اینکه در برخی موارد، زمانی که از `printf` در تابع `CopyFile` استفاده می‌کنید، متد `Error()` فراخوانی نمیشه.

برای درک بهتر این موضوع، باید توجه کنید که تابع `CopyFile` از نوع `io.Copy` استفاده می‌کنه تا محتوای فایل را از فایل مبدأ به فایل مقصد کپی کنه. اگر خطا در زمان کپی رخ بده، خطای مربوطه توسط `io.Copy` برگردونده میشه و این خطا به `err` تعلق میگیره.

توی تابع `main`، وقتی شما `fmt.Printf("%s\\n", err)` رو فراخوانی میکنید، فراخوانی متد `Error()` از نوع `IOError` انجام می‌شود، چونکه متغیر `err` از نوع `*IOError` هستش. اما در تابع `CopyFile`، وقتی `printf` رو استفاده میکنید، `err` به عنوان یک مقدار از نوع `error` استفاده میشه، در نتیجه فراخوانی متد `Error()` از نوع `IOError` نمیشه.


برای فهمیدن ترتیب callها و روند فراخوانی متد `Error()`، میتونید توی تابع `IOError` از یک `fmt.Println` یا `log.Println` استفاده کنید:

func (err IOError) Error() string {
	fmt.Println("Calling Error() method of IOError")
	return fmt.Sprintf("fileName: err input/output : %s msg: %s details: %s", err.FileName, err.Message, err.Err.Error())
}

با این تغییر، متن "Calling Error() method of IOError" هنگام فراخوانی متد `Error()` نشون میشه و مشخص میشه که متد `Error()` در کدوم موارد فراخوانی میشه.

توجه کنید که فراخوانی متد `Error()` در تابع `main` صورت میگیره، چون متغیر `err` از نوع `*IOError` هستش و متد `Error()` در `IOError` تعریف شده .


موفق باشید?

Reza Mobaraki ۰۹ تیر ۱۴۰۲، ۱۶:۵۹

مرسی از پاسختون ولی همچنان پاسخ شما برام مبهم هست

توجه کنید زمانی که ما با استفاده از کدر زیر یک مقداری را print میکنیم

fmt.Printf("%s\\n", err)

همان طور مشخص هست تابع به صورت variadic آرگومان می‌پذیره آن هم از نوع interface

func fmt.Printf(format string, a ...any) (n int, err error)

اگر تابع printf را trace کینم می‌رسیم به متدی بنام`() handleMethods‍` که به صورت زیر است

func (p *pp) handleMethods(verb rune) (handled bool) {
    if p.erroring {
        return
    }
    if verb == 'w' {
        // It is invalid to use %w other than with Errorf, more than once,// or with a non-error arg.
        err, ok := p.arg.(error)
        if !ok || !p.wrapErrs || p.wrappedErr != nil {
            p.wrappedErr = nil
            p.wrapErrs = false
            p.badVerb(verb)
            return true
        }
        p.wrappedErr = err
        // If the arg is a Formatter, pass 'v' as the verb to it.
        verb = 'v'
    }
    // Is it a Formatter?if formatter, ok := p.arg.(Formatter); ok {
        handled = true
        defer p.catchPanic(p.arg, verb, "Format")
        formatter.Format(p, verb)
        return
    }
    // If we're doing Go syntax and the argument knows how to supply it, take care of it now.if p.fmt.sharpV {
        if stringer, ok := p.arg.(GoStringer); ok {
            handled = true
            defer p.catchPanic(p.arg, verb, "GoString")
            // Print the result of GoString unadorned.
            p.fmt.fmtS(stringer.GoString())
            return
        }
    } else {
        // If a string is acceptable according to the format, see if// the value satisfies one of the string-valued interfaces.// Println etc. set verb to %v, which is "stringable".switch verb {
        case 'v', 's', 'x', 'X', 'q':
            // Is it an error or Stringer?// The duplication in the bodies is necessary:// setting handled and deferring catchPanic// must happen before calling the method.switch v := p.arg.(type) {
            case error:
                handled = true
                defer p.catchPanic(p.arg, verb, "Error")
                p.fmtString(v.Error(), verb)
                return
            case Stringer:
                handled = true
                defer p.catchPanic(p.arg, verb, "String")
                p.fmtString(v.String(), verb)
                return
            }
        }
    }
    return false
}

همان طور که در کد بالا مشخص هست وقتی ورودی printfاز جنس error باشه رفتار دیگه ای باهاش انجام میده و تابع `Error()` رو براش فراخوانی میکنه میشه این قسمت کد :

        switch verb {
        case 'v', 's', 'x', 'X', 'q':
            // Is it an error or Stringer?// The duplication in the bodies is necessary:// setting handled and deferring catchPanic// must happen before calling the method.switch v := p.arg.(type) {
            case error:
                handled = true
                defer p.catchPanic(p.arg, verb, "Error")
                p.fmtString(v.Error(), verb)
                return
            case Stringer:
                handled = true
                defer p.catchPanic(p.arg, verb, "String")
                p.fmtString(v.String(), verb)
                return
            }

حال برام چند تا سوال پیش میاد

1- با توجه به این که error یک interface هست به صورت زیر

type error interface {
    Error() stirng  
 }

و ما در آن را در IOError به صورت زیر بازنویسی کردیم چطور متوجه میشود که باید متد Error مربوط به استراک IOError را باید اجرا کند ولی داخل تابع CopyFile هنگام پرینت err سراغ متد Error() مربوط به IOError نمیره ؟ شاید ما strunct‌های دیگه ای داشته باشیم که هرکدام بخواهند متد Error را برای خودشان بازنویسی کنند ممنون میشم این قسمت را توضیح بدید تو کدوم قسمت از handlemethod یا متد دیگر متوجه مییشه این err که print میکنیم مربوط به چه خطایی هست تا متد Error() مربوط به آن را اجرا کند

type IOError struct {
    FileName string
    Message  string
    Err      error
}
func (error *IOError) Unwrap() error {
    return error.Err
}
func (error IOError) Error() string {
    return fmt.Sprintf("IO error occurred: FileName: %s Message: %s Detail: %s", error.FileName, error.Message, error.Err.Error())
}

۲- اگر جنس خطا با در زمان کپی از نوع io.copy هست چطور میتونم برای آن هم یک wrraper بنویسم که خطای آن را هم باز نویسی کنم

bidoo ۱۰ تیر ۱۴۰۲، ۰۹:۵۵

سلام بچه ها

امیدوارم که حالتون خوب باشه

اگه به پاراگراف سوم پاسخ رضا دقت کنی توضیح داده علت این مسئله چیه، با این حال برای اینکه مسئله شفاف‌تر بشه من توضیح میدم باز اگه شفاف نشد حتما بپرس

  • در مورد تابع printf : وقتی این تابع یک error رو میگیره، در نهایت میره سراغ تابع Error اون متغیری که پاس داده شده و عملیات چاپ رو با محتوای این تابع انجام میده.

حالا ما چند جا داریم Printf رو کال میکنیم بریم باهم یکی یکی بررسی کنیم.

1- اینجا یک متغیر err از جنس اینترفیس error بعنوان خروجی از تابع io.Copy بر میگرده نکته ای که مهمه و رضا بهش اشاره کرد اینه که این متغیر از جنس IOError ما نیست و بنابراین در زمان چاپ، خروجی تابع Error ازمتغیر err برگشتی چاپ میشه.

_, err = io.Copy(dst, src)
    fmt.Printf("**************%s***************\\n", err)
// **************write dst.txt: file already closed***************


2- اینجا یک متغیر err از تابع CopyFile خودمون داره برمیگرده و بعدش چاپ انجام میشه، برای اینکه بدونیم تایپش چیه باید ببینیم چه چیزی بعنوان خروجی از تابع CopyFile بر میگرده.

err := CopyFile("src.txt", "dst.txt")
fmt.Printf("%s\\n", err)
// IO error occurred: FileName: src.txt Message: during copy file could not copy Detail: write dst.txt: file already closed

خروجی تابع در همه حالات خطا با الگوی زیر هستش:

return &IOError{FileName: srcName, Message: "----", Err: err}

در نتیجه نوع خروجی خطادار تابع CopyFile از جنس IOError خواهد بود و خروجی تابع Error از متغیر IOError چاپ خواهد شد. یعنی خطی که داریم میبینیم:

func (error IOError) Error() string {
    return fmt.Sprintf("IO error occurred: FileName: %s Message: %s Detail: %s", error.FileName, error.Message, error.Err.Error())
}


3- اینجا همون err قبلی که از جنس IOError هستش پس ازUnwrap شدن داره به Printf پاس داده میشه

err := CopyFile("src.txt", "dst.txt")
fmt.Printf("Unwrap Error: %s", errors.Unwrap(err))
// Unwrap Error: write dst.txt: file already closed

وقتی Unwrap انجام میشه اون error داخلی ( همون خروجی متد Unwrap) برگشت داده میشه و بعدش میره برای چاپ.


پاسخ به سوالات شما:

چطور متوجه میشود که باید متد Error مربوط به استراک IOError را باید اجرا کند ولی داخل تابع CopyFile هنگام پرینت err سراغ متد Error() مربوط به IOError نمیره ؟

نکته ای که باید بهش توجه کنیم اینه که، درسته ما یه Custom error ساختیم ولی وقتی که از standard library‌ها یا پکیج‌های بیرونی استفاده میکنیم اونها نسبت به تایپ هایی که ما ساختیم آگاهی ندارند. در واقع همونطوری که توی سورس زبان نشون دادی تابع Printf میره سراغ خروجی تابع Error و قرار نیست اون بفهمه در چه صورتی بره سراغ تابع Error متغیری از جنس IOError یا از جنس دیگه ای، بلکه این ما هستیم که اون متغیر رو پاس میدیم. یکبار از جنس IOError پاس میدیم و بار دیگه از جنس دیگه ای.


اگر جنس خطا با در زمان کپی از نوع io.copy هست چطور میتونم برای آن هم یک wrraper بنویسم که خطای آن را هم باز نویسی کنم؟

با استفاده از همون Custom error که شما ساختی و wrap کردن error اولیه در آن که انجام دادی

حامد نعیمایی ۱۴ تیر ۱۴۰۲، ۰۸:۳۹