Vivasoft-logo

এরর এবং টেস্টিং(Errors and Testing )

[৬.১] এরর (Error )

প্রোগ্রামিংয়ে, এরর বলতে ভুল বা ত্রুটি বোঝায় যা একটি প্রোগ্রামকে ঠিক মতো এক্সিকিউট হতে বাধা দেয়। ভুল সিনট্যাক্স, ত্রুটিপূর্ণ লজিক, অপ্রত্যাশিত ইনপুট , ডিপেনডেন্সি জাতীয় সমস্যাগুলো ছাড়াও নানান কারণে এরর ঘটতে পারে। আশা করি পূর্ব অভিজ্ঞতা থেকে আমরা কিছু প্রোগ্রামিং এররের সাথে ইতোমধ্যেই পরিচিত। এর মধ্যে কিছু বহুল পরিচিত এরর গুলো হলো – 

  • সিনট্যাক্স এরর
  • রানটাইম এরর
  • কম্পাইলেশন এরর
  • লজিক্যাল এরর

[৬.১.১] Go তে এরর হ্যান্ডলিং, উদ্দেশ্য এবং প্রয়োজনীয়তা

এরর প্রোগ্রামিং এর একটা অবিচ্ছেদ্দ্য অংশ তাই এরর হ্যান্ডেলিং এর বিষয়টা Go এর ক্ষেত্রেও খুবই গুরুত্বপূর্ণ। 

Go মূলত কোনো ফাংশনের শেষ রিটার্ন ভ্যালু হিসেবে  error টাইপের একটি ভ্যালু রিটার্ন করে এরর হ্যান্ডেল করে থাকে। বর্তমানে Go ডেভেলপারদের মধ্যে সবথেকে বড় চ্যালেঞ্জগুলোর মধ্যে উপরের দিকেই থাকবে এরর হ্যান্ডলিং। তাই এটাকে কম গুরুত্ব দিয়ে বা পাশ কাটিয়ে যাওয়া কোনোভাবেই উচিত হবে না। 

অন্যান্য মূলধারার প্রোগ্রামিং ল্যাংগুয়েজগুলো যেমন Java, Javascript,Python বা C# নিয়ে পূর্বে কাজ করে থাকলে হয়ত জানা আছে যে, বেশিরভাগ ল্যাংগুয়েজগুলোই এক্সেপশন ব্যবহার করে। একটি সিস্টেমে যখন একটা এক্সেপশন থ্রো করা হয় তখন কোড সেখানেই থেমে গিয়ে এই এক্সেপশন হ্যান্ডেল করার কোনো কোড ব্লকের খোঁজে নেমে পড়ে। এই এক্সেপশন হ্যান্ডলার ঠিক করে দেয় যে পরবর্তী ধাপ কি হবে এবং কোনো হ্যান্ডলার না পাওয়া গেলে কোড ক্র্যাশ করবে বা বন্ধ হয়ে যাবে –

অন্যান্য প্রোগ্রামিং ল্যাংগুয়েজগুলোর বিপরীতে, Go এররকে অনিবার্য হিসাবে মেনে নিয়ে কাজ করে। বর্তমানে ব্যাক-এন্ড সার্ভিসগুলোর ক্ষেত্রে  একটি সার্ভিসকে প্রায়শই বাইরের API কল, ডাটাবেসে read এবং write এবং অন্যান্য সার্ভিসগুলোর  সাথে যোগাযোগ করতে হয়। উপরের কাজগুলোর মধ্যে যেকোনোটা ফেইল হতে পারে এমনকি ডাটা পার্সিং বা ভ্যালিডেট করার ক্ষেত্রেও। যেহেতু  Go মাল্টিপল রিটার্ন ভ্যালু সাপোর্ট করে তাই এই কলগুলো থেকে যে এররও রিটার্ন হতে পারে এটা ধরে নিয়েই এরর হ্যান্ডেল করতে হবে। 

Go -এর এরর হ্যান্ডেলিং এর পদ্ধতিটি,  মানে এররকে একটি বিকল্প রিটার্ন ভ্যালু হিসাবে চিন্তা করার বিষয়টা সম্পূর্ণ ভিন্ন অন্যান্য ল্যাংগুয়েজ থেকে।

এরর হ্যান্ডেলিং এর সময় হয়ত একটা নির্দিষ্ট কোড ব্লক বার বার ব্যবহার করতে হতে পারে – 

				
					 res, err := doSomething()
   if err != nil {
       // Handle error
   }

				
			

এবং একটা সময় পর মনে হবে কিবোর্ডে এইরকম একটি বাটন থাকলে মনে হয় খুব ভালো হতো –

Go-তে এরর কে  রিপ্রেজেন্ট করা হয় বিল্ট ইন এরর ইন্টারফেস এর মাধ্যমে  যেটাতে শুধুমাত্র একটি মেথড রয়েছে।

				
					type error interface {
       Error() string
   }

				
			

মূলত, এরর হচ্ছে এমন কিছু যা কিনা এই  Error() মেথড সিগনেচারকে ইমপ্লিমেন্ট করে এবং string  হিসেবে একটি এরর মেসেজ রিটার্ন করে।

Go-তে প্রধানত দুইধরণের  এরর রয়েছে – 

  1. বিল্ট ইন এরর (Built-in Error): Go-তে, বেশ কিছু বিল্ট ইন এরর রয়েছে, যা সচরাচর  একটি প্রোগ্রাম রান করার সময় ঘটতে পারে। এই এররগুলো ইউজারকে আরও অর্থপূর্ণ এরর মেসেজ দিতে এবং কোডে সমস্যাগুলো ডিবাগে সহায়তা করতে, ব্যবহার করা যেতে পারে।

Go-তে বিল্ট-ইন এররের একটি মজার উদাহরণ হল math.ErrNaN এরর। মূলত গণনার ফলাফল ভ্যালিড সংখ্যা না হয়ে থাকলে এই এরর রিটার্ন হয়। একটি ঋণাত্মক সংখ্যার বর্গমূল নেওয়ার চেষ্টা করার সময় এই ত্রুটি ঘটতে পারে, উদাহরণস্বরূপঃ-

				
					 _, err := math.Sqrt(-1)
   if err == math.ErrNaN {
       fmt.Println("Oops, I think I just broke math!")
   }

				
			

Go-তে আরেকটি বিল্ট-ইন এরর হল os.ErrNotExist এরর। যখন কোনো  ফাংশন এমন কোনো ফাইল বা ডিরেক্টরির অ্যাক্সেস করতে চায় যেটা এক্সিস্ট করে না, সেসব পরিস্থিতিতে এই এররটি ব্যবহার করা যেতে পারে –

				
					
   file, err := os.Open("nonexistent-file.txt")
   if err != nil && os.IsNotExist(err) {
       fmt.Println("Sorry, I can't find that file.")
   }

				
			

2. কাস্টম এরর (Custom Error): বিল্ট-ইন এররগুলো ছাড়াও, Go ডেভেলপারদের errors.New() ফাংশন (যেটা সম্পর্কে আমরা একটু পরেই বিস্তারিত জানবো) বা এরর ইন্টারফেস ব্যবহার করে কাস্টম এরর টাইপ তৈরি করার সুযোগ দেয় যার সাহায্যে আমরা নিজেদের মতো করে এরর মেসেজ ঠিক করতে পারি। এটি ডেভেলপারদের আরও অর্থপূর্ণ এরর মেসেজ প্রদান করতে এবং এরর সম্পর্কে তথ্য এনক্যাপসুলেট করার সুযোগ দেয় –

				
					 type MyError string

   func (e MyError) Error() string {
       return string(e)
   }

   func myFunc() error {
       // do something...
       return MyError("Something went wrong!")
   }

				
			

মূলত এখানে MyError এ, error ইন্টারফেসে বিদ্যমান Error() মেথড সিগনেচারকে ইমপ্লিমেন্ট করার মাধ্যমে আমরা নিজেদের মতো করে কাস্টম এরর মেসেজ রিটার্ন করতে পারি।

এছাড়াও চাইলে কাস্টম Struct দিয়েও এররকে রিপ্রেজেন্ট করা যায়, উদাহরণস্বরূপ –

				
					  package main

   import "fmt"

   func main() {
       user := new(User)
       user.FirstName = "Kawsar"
       err := myFunc(*user)
       fmt.Println(err.Error())
   }

   type MyError struct {
       Code    int
       Message string
   }

   func (e MyError) Error() string {
       return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
   }

   type User struct {
       FirstName string
       LastName  string
   }

   func myFunc(user User) error {
       if user.FirstName == "" {
           return MyError{Code: 400, Message: "First name is required"}
       }
       if user.LastName == "" {
           return MyError{Code: 400, Message: "Last name is required"}
       }
       return nil
   }


   //Output: Error 400: Last name is required

				
			

এছাড়াও আরও কিছু এরর হলঃ

  • রানটাইম এরর (Runtime Error): এগুলো এমন এরর যা প্রোগ্রামের রান টাইমে ঘটে, যেমন শূন্য দ্বারা একটি বিভাজন, অ্যারের বাইরের উপাদান অ্যাক্সেস করার চেষ্টা করা ইত্যাদি।

 

  • ফাংশন দ্বারা রিটার্নকৃত এরর (Errors Returned by Functions): এক্ষেত্রে কোনো ফাংশন যখন সফলভাবে তার অপারেশন গুলো সম্পূর্ণ করতে পারেনা তখন এরর রিটার্ন করে। Go-তে, যে ফাংশনগুলো এরর রিটার্ন দিতে পারে সেগুলোর সাধারণত একটি রিটার্ন টাইপ থাকে (resultType, error)৷ এরর কন্ডিশন সত্য হলে এরর ভ্যালু  আর এরর না হলে কাঙ্খিত ফলাফল রিটার্ন করে –
				
					  func divide(a, b float64) (float64, error) {
           if b == 0 {
               return 0, errors.New("division by zero")
           }
           return a / b, nil
       }

				
			

এই উদাহরণে, divide() ফাংশন আর্গুমেন্ট হিসাবে দুটি float64 সংখ্যা নেয় এবং একটি float64 মান ও একটি এরর রিটার্ন করে। দ্বিতীয় আর্গুমেন্ট শূন্য হলে, ফাংশন errors.New() ফাংশন ব্যবহার করে একটি কাস্টম এরর বার্তা সহ একটি এরর রিটার্ন করে। অন্যথায়, এটি দুটি সংখ্যার ভাগফল গণনা করে এবং একটি নিল এরর রিটার্ন করে।

[৬.১.২] errors প্যাকেজ

errors প্যাকেজ হল Go প্রোগ্রামিং ল্যাঙ্গুয়েজে একটি বিল্ট ইন প্যাকেজ যার সাহায্যে আমরা সহজেই এরর তৈরি এবং ম্যানিপুলেট করতে পারবো। এখন আমরা  এই প্যাকেজের সচরাচর ব্যবহৃত ফাংশনগুলো নিয়ে ধারনা নিবোঃ-

  1. Is():
				
					  func Is(err, target error) bool

				
			

পাস করা এরর এবং টার্গেট এরর একই কিনা তা চেক করতে Is ফাংশনটি ব্যবহার করা হয়, সমান হলে true, না হলে false রিটার্ন করে  –

				
					package main

   import (
       "errors"
       "fmt"
       "io/fs"
       "os"
   )

   func main() {
       if _, err := os.Open("non-existing"); err != nil {
           if errors.Is(err, fs.ErrNotExist) {
               fmt.Println("file does not exist")
           } else {
               fmt.Println(err)
           }
       }
   }


   //Output:
   //file does not exist
				
			

2. New():

				
					   func New(text string) error
				
			

এই ফাংশনটি একটি string আর্গুমেন্ট নেয় এবং সেই string অনুযায়ী একটি নতুন এরর রিটার্ন করে –

				
					

   func main() {
       err := errors.New("something went wrong")
       if err != nil {
           fmt.Print(err)
       }
   }

				
			

 3. As():

				
					  func As(err error, target any) bool
				
			

এই ফাংশনটি, errors.Is() এর অনুরূপ এক ধরনের এরর এবং একটি এরর টাইপ আরগুমেন্ট হিসেবে নেয় তারপরে এটি পাস কৃত এররটির চেইনের থাকা অন্যসব এররের  টাইপের সাথে প্রদত্ত এরর টাইপ মেলে কিনা তা চেক করে । যদি একটি এরর এরও টাইপ মিলে যায়, তাহলে ফাংশনটি true রিটার্ন করে, না হলে এটি false রিটার্ন করে –

				
					func main() {
       if _, err := os.Open("non-existing"); err != nil {
           var pathError *fs.PathError
           if errors.As(err, &pathError) {
               fmt.Println("Failed at path:", pathError.Path)
           } else {
               fmt.Println(err)
           }
       }
   }


   //Output:
   //Failed at path: non-existing

				
			

4. Join():

				
					 func Join(errs ...error) error

				
			

এই ফাংশনটি অনির্দিষ্ট সংখ্যক এরর আরগুমেন্ট হিসেবে নেয় এবং সবগুলো এররকে এক করে একটি সিঙ্গেল এরর মেসেজ রিটার্ন করে –

				
					package main

   import (
       "errors"
       "fmt"
   )

   func main() {
       err1 := errors.New("err1")
       err2 := errors.New("err2")
       err := errors.Join(err1, err2)
       fmt.Println(err)
       if errors.Is(err, err1) {
           fmt.Println("err is err1")
       }
       if errors.Is(err, err2) {
           fmt.Println("err is err2")
       }
   }

				
			

আউটপুট – 

				
					err1
   err2
   err is err1
   err is err2

				
			

এই প্যাকেজ এর ফাংশংন গুলো সম্পর্কে আরও বিস্তারিত জানতে এই লিংকটি কাজে আসবে।

[৬.১.৩] প্যানিক ও রিকোভার (Panic & Recover)

Go তে এরর হ্যান্ডেলিং এর জন্য panic এবং recover নামে গুরুত্বপূর্ণ দুটি বিল্ট ইন ফাংশন রয়েছে যেগুলো দিয়ে আমরা প্রোগ্রামের কিছু অপ্রত্যাশিত সিচুয়েশন হ্যান্ডেল করে প্রোগ্রামকে ক্র্যাশ করা থেকে রক্ষা করি।

Panic

panic হচ্ছে এমন একটা ফাংশন যা প্রোগ্রামের নরমাল এক্সিকিউশন তাৎক্ষনিক বন্ধ করে, সমস্ত defer ফাংশনগুলো এক্সিকিউট করে এবং একটি লগ মেসেজ প্রিন্ট হয়, যাতে প্যানিক ভ্যালু (সাধারণত একটি এরর মেসেজ) এবং একটি স্ট্যাক ট্রেস(একটা নির্দিষ্ট সময়ে কল স্ট্যাকে থাকা সবগুলো ফাংশন কল) অন্তর্ভুক্ত থাকে।

panic বিভিন্ন কারণে ঘটতে পারে, যেমন একটি নিল পয়েন্টার অ্যাক্সেস করার চেষ্টা করা, একটি সংখ্যাকে শূন্য দিয়ে ভাগ করা, বা সীমার বাইরের অ্যারে ইন্ডেক্স অ্যাক্সেস করার চেষ্টা করা ইত্যাদি। সাধারণত, panic নির্দেশ করে যে প্রোগ্রামে অপ্রত্যাশিত এবং গুরুতর কিছু ঘটেছে যা প্রোগ্রামটি নরমালি হ্যান্ডেল করতে পারছে না।

				
					 func main() {
       divide(5)
   }

   func divide(x int) {
       fmt.Printf("divide(%d) \n", x+0/x)
       divide(x - 1)
   }

				
			

একবার শূন্য ব্যবহার করে ডিভাইড ফাংশন কল করা হলে, প্রোগ্রামটি panic করবে , যার ফলে নিম্নলিখিত আউটপুট হবে –

				
					 divide(5)
   divide(4)
   divide(3)
   divide(2)
   divide(1)
   panic: runtime error: integer divide by zero
   Goroutine 1 [running]:
   main.divide(0x8053a8?)
       d:/GoLang/Gobyexample/dummy.go:10 +0xa5
   main.divide(0x1)
       d:/GoLang/Gobyexample/dummy.go:11 +0x94
   main.divide(0x2)
       d:/GoLang/Gobyexample/dummy.go:11 +0x94
   main.divide(0x3)
       d:/GoLang/Gobyexample/dummy.go:11 +0x94
   main.divide(0x4)
       d:/GoLang/Gobyexample/dummy.go:11 +0x94
   main.divide(0x5)
       d:/GoLang/Gobyexample/dummy.go:11 +0x94
   main.main()
       d:/GoLang/Gobyexample/dummy.go:6 +0x1e
   exit status 2

				
			

আমরা নিজস্ব প্রোগ্রামে বিল্ট ইন panic ফাংশন ব্যবহার করতে পারি –

				
					func getArguments() {
       if len(os.Args) == 1 {
           panic("Not enough arguments!")
       }
   }

				
			

panic  ফাংশন মূলত এমনসব ক্ষেত্রেই ব্যবহার করা উচিত যখন এমন কিছু ঘটে যা প্রোগ্রামটি আশা করেনা বা নরমাল ওয়েতে হ্যান্ডেল করার আর কোনো উপায় থাকে না।

নিচের উদাহরণে দেখানো হয়েছে কিভাবে, অ্যাপ্লিকেশনটি panic করার আগে defer ফাংশনগুলো কার্যকর করা হয়ে থাকে –

				
					  func main() {
       accessSlice([]int{1, 2, 5, 6, 7, 8}, 0)
   }

   func accessSlice(slice []int, index int) {
       fmt.Printf("item %d, value %d \n", index, slice[index])
       defer fmt.Printf("defer %d \n", index)
       accessSlice(slice, index+1)
   }

				
			

প্রোগ্রামের আউটপুট –

				
					 item 0, value 1
   item 1, value 2
   item 2, value 5
   item 3, value 6
   item 4, value 7
   item 5, value 8
   defer 5
   defer 4
   defer 3
   defer 2
   defer 1
   defer 0
   panic: runtime error: index out of range [6] with length 6
   Goroutine 1 [running]:
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x6)
       d:/GoLang/Gobyexample/dummy.go:10 +0x186
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x5)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x4)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x3)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x2)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x1)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.accessSlice({0xc000107f40?, 0x6, 0x6}, 0x0)
       d:/GoLang/Gobyexample/dummy.go:12 +0x15c
   main.main()
       d:/GoLang/Gobyexample/dummy.go:6 +0x70
   exit status 2

				
			

panic ফাংশনটি খুবই সতর্কতার সহিত এবং শুধুমাত্র রিকভার করা যায় না এমন এররের ক্ষেত্রেই ব্যবহার করা উচিত।

উদাহরণস্বরূপ, যদি একটি ফাংশন একটি ভুল প্যারামিটার পায়, তবে এটি panic করার পরিবর্তে একটি এরর রিটার্ন  করতে পারে। কিন্তু যদি প্রোগ্রামটি একটি অসামঞ্জস্যপূর্ণ অবস্থায় থাকে, বা যদি এটি একটি গুরুতর এররের সম্মুখীন হয় যা এটি হ্যান্ডেল করতে ব্যর্থ হয় , তাহলে এক্ষেত্রে  panic করা উপযুক্ত হতে পারে।

যাইহোক, এটি লক্ষ্য করা গুরুত্বপূর্ণ যে panic শুধুমাত্র ব্যতিক্রমী ক্ষেত্রেই ব্যবহার করা উচিত, নরমাল কন্ট্রোল ফ্লো নিয়ন্ত্রণ প্রক্রিয়া হিসাবে নয়।

 

Recover 

কিছু কিছু ক্ষেত্রে panic এর কারনে ক্র্যাশ হয়ে যাওয়া কোনো অ্যাপ্লিকেশনকে বন্ধ করা উচিত নয় বরং পুনরুদ্ধার করা উচিত। Go-তে, “recover ” কিওয়ার্ডটি একটি panic করা গো-রুটিন এর নিয়ন্ত্রণ পুনরুদ্ধার করতে ব্যবহৃত হয়।

একটি recover ফাংশন সর্বদা একটি defer ফাংশনের ভিতরে কল করা উচিত কারণ defer ফাংশনটি প্রোগ্রাম panic করলেও কাজ করা বন্ধ করে না, তাই এর ভিতরে থাকা recover ফাংশনটি panic বন্ধ করে দেয় এবং panic এর এরর ভ্যালু রিটার্ন করে –

				
					 package main

   import "fmt"

   func main() {
       accessSlice([]int{1, 2, 5, 6, 7, 8}, 0)
   }

   func accessSlice(slice []int, index int) {
       defer func() {
           if p := recover(); p != nil {
               fmt.Printf("internal error: %v", p)
           }
       }()
       fmt.Printf("item %d, value %d \n", index, slice[index])
       defer fmt.Printf("\n defer %d", index)
       accessSlice(slice, index+1)
   }

				
			

প্রোগ্রামের আউটপুট –

				
					item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6
 defer 5
 defer 4
 defer 3
 defer 2
 defer 1
 defer 0
				
			

উপরে আমরা যে কোড করেছি সেখানে ইন্ডেক্স 6 হলেই ফাংশনটি  panic করে defer করা কাজ গুলো শেষ করে ক্র্যাশ করছিলো কিন্তু একটি recover ফাংশন যোগ করার পরে আমরা দেখছি এখন আর আগের মতো প্রোগ্রামটি ক্র্যাশ  না করে নরমাল কন্ট্রোল ফ্লো অনুসারে চলছে, কারন panic করার পর সেটা আমাদের defer করা ফাংশনটিতে থাকা  recover ফাংশনটি ক্যাচ করে এবং প্রোগ্রামটিকে ক্র্যাশ করা থেকে রক্ষা করে।

নিচে কিছু পরিস্থিতি রয়েছে যেখানে recover ফাংশন ব্যবহার করা যেতে পারে:

  • অপ্রত্যাশিত এররগুলো হ্যান্ডেল করার সময়:  যদি একটি ফাংশন এমন কোনো অপ্রত্যাশিত এররের সম্মুখীন হয় যা নরমালি হ্যান্ডেল করার কোনো উপায় নেই, তাহলে প্রোগ্রামটি ক্র্যাশ হওয়া থেকে রক্ষা করার জন্য recover ফাংশন ব্যবহার করা একটি ভাল উপায় হতে পারে । উদাহরণস্বরূপ, যদি একটি ফাইল খোলা না যায় বা একটি নেটওয়ার্ক সংযোগ ব্যর্থ হয়, সেক্ষেত্রে আমরা এররটি হ্যান্ডেল করতে এবং প্রোগ্রাম নরমালি চালিয়ে যেতে recover ফাংশন ব্যবহার করতে পারি।

 

  • রিসোর্স ক্লিন আপ করার সময়:  panic এর কারণে একটি আনস্টেবল অবস্থায় ফেলে রাখা রিসোর্সগুলো ক্লিন আপ বা ফ্রী করতে recover ফাংশন ব্যবহার করা যেতে পারে। যেমন, প্যানিকের সময় কোনো ফাইল খোলা থাকলে, প্রোগ্রামটি বের হওয়ার আগে আমরা ফাইলটি বন্ধ করতে recover ফাংশন ব্যবহার করতে পারি।


দীর্ঘসময় ধরে-চলমান অপারেশনগুলো হ্যান্ডেল করার সময়: যদি একটি ফাংশন দীর্ঘ সময় যাবত চালানোর  সম্ভাবনা থাকে, তবে যে কোনো সময় সেটি panic করতে পারে এধরনের অবস্থা সামলাতে এবং প্রোগ্রামটিকে চলমান রাখতে recover ফাংশন ব্যবহার করা একটি ভাল প্র্যাকটিস হতে পারে। যেমন, এটি সার্ভার বা অন্যান্য দীর্ঘসময় ধরে-চলমান প্রোগ্রামগুলোর জন্য।