[৫.৩] ফাইল অপারেশন (File operations)
Go এর io/ioutil প্যাকেজ ব্যবহার করে আমরা লোকাল সিস্টেম এ ফাইল read-write করতে পারি, এর সাথে os প্যাকেজ ব্যবহার করে আমরা নিজস্ব অপারেটিং সিস্টেম এর বিভিন্ন বিষয় অ্যাক্সেস করতে পারি। যেমন, file opening, closing, reading,creating, writing, remove
[৫.৩.১] ফাইল open
os.Open(“..file_name”) ফাংশন ব্যবহার করে আমরা লোকাল ফাইল সিস্টেমের নির্দিষ্ট ফাইল ওপেন করতে পারি। ব্র্যাকেটের ভিতরে file_name এর জায়গায় আমরা যে ফাইলটা ওপেন করতে চাচ্ছি, তার path দিতে হবে। এই ফাংশনটা একটা os.File এর একটি পয়েন্টার (*os.File, যেটা file handle হিসেবে কাজ করবে) এবং একটা এরর রিটার্ন করে।
ফাইল ওপেন করার পরে, বাকি সব কাজ শেষে ফাইলটা close করা ভালো প্র্যাকটিস ( এটার জন্য defer ব্যবহার করা যায়), নিচের কোডটা খেয়াল করি –
f, err := os.Open("../5-type-converison/read-file.txt")
defer f.Close()
if err != nil {
fmt.Println(err)
return
}
এই কোডের জন্য আমাদের ফাইল স্ট্রাকচার ছিলোঃ
আমরা “6-file/file-op.go” path এ থেকে “5-type-converison” ফোল্ডারের read-file.txt ওপেন করছি। তাই os.Open() এর ভিতরে সেই অনুযায়ী ফাইলের path দেয়া হয়েছে। যদি ঠিকঠাক মতো ফাইল ওপেন না হয়, তাহলে এরর রিটার্ন করতো।
[৫.৩.২] ফাইল Read
কোন ফাইলকে read করার পরে আমরা একটি ফাইল হ্যান্ডেল পাই। এই ফাইল হ্যান্ডেলের সাথে os প্যাকেজের Read() ফাংশন ব্যবহার করে আমরা একটি ফাইল read করতে পারি। তবে এই ফাংশনটি একটা byte টাইপের অ্যারে তে, ফাইল থেকে read করা বাইটগুলো স্টোর করে।
একসাথে কতটুকু read করে এবং স্টোর করে?
byte টাইপের যে অ্যারে তে স্টোর করছি, তার দৈর্ঘ্যের সমান (যদি ফাইল এর দৈর্ঘ্য byte অ্যারের চেয়ে কম থাকে তাহলে সবই read করবে)। রিটার্ন করে কতগুলা byte read করা হয়েছে সেই সংখ্যা এবং এরর (এই এরর চেক করে যদি দেখা যায় যে তা nil, তাহলেই একমাত্র সামনে আগাবে) –
f, err := os.Open("read-file.txt")
defer f.Close()
if err != nil {
fmt.Println(err)
return
}
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(data[:n]))
আউটপুট –
Read 12 bytes: Hello Golang
উপরের কোডটা যদি খেয়াল করি, তাহলে দেখা যাবে, os.Open(“read-file.txt”), read-file.txt ফাইল টা read করে এবং f ফাইল হ্যান্ডেল এবং err, এরর রিটার্ন করে। এই f ফাইল হ্যান্ডল Read() ফাংশন ব্যবহার করে, 100 দৈর্ঘ্যের byte টাইপ অ্যারেতে, আমাদের ব্যবহার করা ফাইল থেকে সর্বোচ্চ 100 byte ডাটা read করতে পারবে।
io/ioutil এর ReadFile() ফাংশন ব্যবহার করে আরো সহজে ফাইল read করা যায়। কেননা এক্ষেত্রে আমাদের আলাদা ভাবে ফাইল হ্যান্ডেল লাগে না, আবার আমাদের আলাদা ভাবে byte অ্যারে ডিক্লেয়ার করে তাতে স্টোর করা লাগে না –
ioutilData, err := ioutil.ReadFile("read-file.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Read %d bytes: %s\n",len(ioutilData), string(ioutilData))
আউটপুট –
Read 12 bytes: Hello Golang
এই কোড যদি খেয়াল করি, ioutil.ReadFile(“read-file.txt“) টি read-file.txt ফাইল read করে এবং রিটার্ন করছে ioutilData (byte টাইপের অ্যারে) এবং err (এরর). এই ioutilData কে string এ পরিবর্তন করে আমরা ফাইলের কন্টেন্ট দেখতে পারি।
[৫.৩.৩] ফাইল Create
os.Create(..file_name) ব্যবহার করে নতুন ফাইল তৈরি করা হয় (যদি ফাইল টা আগে থেকেই থেকে থাকে, তাহলে ফাইলের আগের সবকিছু মুছে যায়)। এই ফাংশন টা ফাইলের path ইনপুট হিসেবে নিয়ে, ফাইল হ্যান্ডেল এবং এরর রিটার্ন করে(যদি এরর nil না হয়, তাহলে ফাইল হ্যান্ডেলকে আর ইনপুট/আউটপুটের এর জন্য ব্যবহার করা যাবে না –
f, err := os.Create("read-file.txt")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
এই কোডের সাহায্যে, একই ডিরেক্টরিতে “read-file.txt” ফাইলে create হবে, যদি কোন এরর না থাকে।
[৫.৩.৪] ফাইল Writing
এখানে একটা নতুন ফাইল create করার পরে write করা হবে। আগে থেকে একটা byte টাইপের অ্যারে তৈরি করা হবে। সেখানে আমাদের ফাইলে কি write করতে চাই, তা স্টোর করা হবে। ফাইল create করার সময় আমরা যে হ্যান্ডেলটা পাবো, তা ব্যবহার করেই write ফাংশন টা ব্যবহার করা হবে। এই ফাংশন টা রিটার্ন করে কত বাইট write করা হচ্ছে তার সংখ্যা এবং এরর। এই এররের সাহায্যে, আমাদের ফাইল write এ কোনো সমস্যা হলো কিনা তা যাচাই করা হয়।
তবে যদি কোন কারণে নতুন create করা file এ write হওয়া byte সংখ্যা এবং আমরা যা write করতে চাচ্ছি তার দৈর্ঘ্য যদি সমান না হয়, তাহলে কোন এরর হবে না, কিন্তু এটা আমাদের আলাদাভাবে চেক করে নিতে হবে।
f, err := os.Create("read-file.txt")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
data := []byte("Hello Golang")
n, err := f.Write(data)
if len(data) != n {
fmt.Printf("line num not matched %v, %v\n", n, len(data))
return
}
if err != nil {
fmt.Println(err)
return
}
ioutil দিয়েও ফাইল write করা যায়। এই ক্ষেত্রে ioutil এর WriteFile() ফাংশন এর –
- ১ম প্যারামিটারঃ যে ফাইলটা write করতে চাই, তার file path
- ২য় প্যারামিটারঃ byte টাইপের অ্যারের নাম (ফাইলে যা স্টোর করে রাখা হবে, সেই কন্টেন্ট এই অ্যারেতে আগে স্টোর করে রাখা হয়)
- ৩য় প্যারামিটারঃ ফাইল permission mode সেট করে দিতে হয়। এর দ্বারাই ফাইলে read/write করার পারমিশন সেট হয়।
data := []byte("Hello, world!")
err = ioutil.WriteFile("read-file.txt", data, 0644)
if err != nil {
fmt.Println(err)
return
}
[৫.৩.৫] ফাইল রিমুভ করা
os.Remove(“file_name”) এর সাহায্যে নির্দিষ্ট করা path এর ফাইলটি ডিলিট হয়ে যাবে। এই ফাংশনটির ইনপুট হিসেবে, শুধুমাত্র একটা ফাইল path নেয় এবং একটি এরর রিটার্ন করে। যদি এরর nil না হয়, তাহলে ফাইল ডিলিট হবে না।
err := os.Remove("created.txt")
if err != nil {
fmt.Println(err)
return
}
যদি ভ্যারিয়েবল null হয়, তাহলে InvalidUnmarshalError দিবে।
নিচের কোডটা খেয়াল করি –
byt := []byte(
`
{
"num":6.13,
"strs":["a","b"]
}
`,
)
var v map[string]interface{}
if err := json.Unmarshal(byt, &v); err != nil {
panic(err)
}
fmt.Println(v)
এখানে আমরা যদি একটি JSON অবজেক্ট কে এমন একটি Map এ পরিবর্তন করতে চাই, যেখানে key গুলো হবে string এবং key এর ভ্যালু যেকোনো কিছুই হতে পারে।
আউটপুট:
map[num:6.13 strs:[a b]]
তবে এই পদ্বতির একটি সমস্যা হলো, প্রতিবার Map এর ভ্যালুগুলো অ্যাক্সেস করার জন্য আমাদের টাইপ কাস্টিং করে নিতে হবে, (কেননা আমরা ভ্যালু গুলা interface{} হিসেবে নিয়েছি)। এটার জন্য যদি কোন ভুল হয় তাহলে রানটাইম এরর হবে, যেটা চিহ্নিত করা খুবই ঝামেলার বিষয়। তাই আমরা যদি আগে থেকেই, ডিকোডেড JSON অবজেক্ট এর ফিল্ডগুলোর ভ্যালু কি টাইপ হবে, তা ঠিক করে দিতে চাই, তাহলে আমরা Struct ব্যবহার করতে পারি –
type response2 struct {
Page int `json:"page-number"`
Fruits []string `json:"fruits-List"`
}
str := `
{
"number": 1,
"fruits-List": ["apple", "peach"]
}`
res := response2{}
json.Unmarshal([]byte(str), &res)
fmt.Println(res)
fmt.Println(res.Fruits[0])
আউটপুট –
{0 [apple peach]}
apple
এই পদ্বতির সবচেয়ে বড় সুবিধা হলো; type-safety এবং প্রতিবার টাইপ কাস্টিং না করা।
json.valid():
যেখানে আমরা স্লাইস / map / চ্যানেল নিয়ে কাজ করছি।
শুরুতে কোনো ভ্যালু দিয়ে এদেরকে ইনিশিয়ালাইজ করে নিবো (নতুবা nil / zero দিয়ে ইনিশিয়ালাইজ হবে)
শুরুতে এদের সাইজ, ক্যাপাসিটি কত হবে তা আমরা জানলেও, পরে তা পরিবর্তন হতে পারে (অর্থাৎ এদের সাইজ, ক্যাপাসিটি ডাইনামিক্যালি পরির্তন হবে)
এরকম অবস্থায়এই ফাংশন ব্যবহার করে আমরা চেক করতে পারি , কোন []byte (বাইট স্লাইস) ভ্যালিড JSON ডাটা কি-না। unmarshal() করার আগে এই ফাংশন ব্যবহার করে আগেই এরর চেক করা হয়.
নিচের কোডটি খেয়াল করি –
type response2 struct {
Page int `json:"page-number"`
Fruits []string `json:"fruits-List"`
}
str := `
{
"number": 1 "fruits-List": ["apple", "peach"]
}`
if json.Valid([]byte(str)) {
res := response2{}
json.Unmarshal([]byte(str), &res)
fmt.Println(res)
fmt.Println(res.Fruits[0])
} else{
fmt.Println("wrong JSON response")
}
আউটপুট –
wrong JSON response
4.json.NewEncoder():
এটি ব্যবহার করে JSON encoding করা যায় এবং এর সুবিধা হলো আমরা একই সাথে JSON এ encode, আউটপুট স্ট্রিম এবং স্টোর করতে পারছি –
package main
import (
"encoding/json"
"os"
)
type Person struct {
Name string
Age int
}
func main() {
person := Person{Name: "John", Age: 30}
file, err := os.Create("person.json")
if err != nil {
panic(err)
}
defer file.Close()
encoder := json.NewEncoder(file)
err = encoder.Encode(person)
if err != nil {
panic(err)
}
}
আউটপুট person.json ফাইলে পাওয়া যাবে –
{"Name":"John","Age":30}
আরো বেশ কিছু ফাংশন রয়েছেঃ এর মধ্যে json.NewDecoder() হলো json.NewEncoder() এর ঠিক উল্টো, যেটা json.NewDecoder() ইনপুট স্ট্রিম থেকে সরাসরি JSON ফরম্যাটে ইনপুট নেয়।
[৫.২.২] Data Serialization
Data Serialization হলো স্ট্রাকচার্ড ডাটা কে এমন একটা ফরম্যাটে পরিবর্তন করা, যেটাকে নেটওয়ার্কে ট্রান্সমিট করা যাবে অথবা Disk এ সেইভ করে রাখা যাবে, যাতে পরে এই ডাটা কে ব্যবহার করা যায়।
Go তে Data Serialization এর জন্য সবচেয়ে বেশি ব্যবহৃত টেকনিক হলো JSON ফরম্যাটে পরিবর্তন করে নেয়া, যেটা আমরা ইতিমধ্যে শিখেছি।
এখানে খেয়াল করতে হবে যে, প্রায়শঃ JSON ফরম্যাটের key এবং Struct এর নির্দিষ্ট ফিল্ড গুলো একই নামের হয় না, তাই Struct এর ফিল্ডের সাথে কাস্টমাইজড “json” ট্যাগ ব্যবহার করা হয়। নিচের কোডটি খেয়াল করি –
type response2 struct {
Page int `json:"pageNumber"`
Fruits []string `json:"fruits-List"`
}
jsonStr := `
{
"page-number": 1,
"fruits-List": ["apple", "peach"]
}`
res := response2{}
json.Unmarshal([]byte(jsonStr), &res)
fmt.Println(res)
আউটপুট –
{0 [apple peach]}
এই ক্ষেত্রে, আমরা যে জেসন ডাটা “jsonStr” রিসিভ করে, এনকোড করতে চাচ্ছি, তার একটা key (“page-number”), response2 Struct এর Page ফিল্ডের জেসন ট্যাগ (pageNumber) এর সাথে মিলে নি। এর ফলে আমরা যখন jsonStr কে এনকোড করে response2 Struct এ স্টোর করে রাখতে চাচ্ছি, ওই ফিল্ডের ভ্যালু null হয়ে যাচ্ছে (কোন error পাইনি কিন্তু!!) । তাই, Struct এর “json” ট্যাগ দেয়ার সময় আমাদের বাড়তি সতর্কতা অবলম্বন করা লাগবে, যেন JSON এর key এবং Struct এর একই ফিল্ডের “json” ট্যাগ একই হয়।
[৫.২.৩] টাইপ কনভার্শন
এক টাইপের ডাটা কে অন্য টাইপের ডাটা তে পরিবর্তন করাকে বলে টাইপ কনভার্শন. Go তে এক টাইপের ডাটা কে অন্য টাইপে পরিবর্তন করা খুবই সহজ –
var iNum int = 10
var fNum float64 = float64(iNum)
fmt.Printf("value = %v | type - %T\n", fNum, fNum)
var fNum2 = 5.65
iNum2 := int(fNum2)
fmt.Printf("value = %v | type - %T\n", iNum2, iNum2)
আউটপুট –
value = 10 | type - float64
value = 5 | type - int
এই কোড দেখে খুব সহজেই বোঝা যাচ্ছে যে, আমরা যদি V টাইপের ডাটা কে T টাইপের ডাটাতে পরিবর্তন করতে চাই, তাহলে আমরা T(V) ব্যবহার করবো।
তবে অবশ্যই কিছু নিয়ম মেনে করা লাগবে, string টাইপের ডাটা কে তো আর ইন্টিজার / ফ্লোট এ পরিবর্তন করতে চাওয়া উচিত না।
var str string = "string value"
iNum2 := int(str)
এই অদ্ভুত চাওয়ার ফলাফল হাতে-নাতে –
# command-line-arguments
./type_conv.go:8:15: cannot convert str (variable of type string) to type int
গাণিতিক হিসাব নিকাশ করার সময় আমাদের এই টাইপ কনভার্শন গুলো খেয়াল রেখে করা লাগবে।
ধরি, আমরা চাচ্ছি ২ টি সংখ্যা যোগ করবো, যেখানে একটি interger, আরেকটি ফ্লোট. তখন অবশ্যই এদেরকে একই টাইপে পরিবর্তন করে যোগ করা লাগবে, নাহলে error আসবে
var num1 float64 = 10.5
var num2 int = 5
result := num1 + num2
fmt.Println("result = ", result)
আউটপুট –
# command-line-arguments
./type_conv.go:20:12: invalid operation: num1 + num2 (mismatched types float64 and int)
স্পষ্টতই, দেখা যাচ্ছে,আমাদের যোগ অপারেশনে ভুল হয়েছে।
সঠিক ভাবে করলে হবে নিচের মতো –
var num1 float64 = 10.5
var num2 int = 5
result := float64(num1) + float64(num2)
fmt.Println("result = ", result)
এখানে ২ টা নাম্বারকেই ফ্লোটে পরিবর্তন করে নেয়া হয়েছে, চাইলে আমরা ইন্টিজারেও পরিবর্তন করে নিতে পারতাম।
আমরা চাইলে বিভিন্ন ফরম্যাটে ব্যবহার করে বিভিন্ন বেসের নাম্বার কে অন্য কোন বেইসে আউটপুট স্ট্রিমে দেখাতে পারিঃ
var d1 int = 10
fmt.Printf("in binary=%b\n", d1)
fmt.Printf("in octal=%o\n", d1)
fmt.Printf("in hexadecimal=%X\n", d1)
আউটপুট –
in binary=1010
in octal=12
in Hexadecimal=A