[৫.২] ডাটা পার্সিং এবং ম্যানিপুলেশন (Data parsing & manipulation)
[৫.২.১] JSON
JSON এর পূর্ণরূপ হলো JavaScript Object Notation, যেটা একটি বহুল ব্যবহৃত ডাটা ইন্টারচেঞ্জ – ফরম্যাট. সাধারণত ওয়েব অ্যাপ্লিকেশন এবং API ডেভলপাররা ভিন্ন অ্যাপ্লিকেশন এবং সিস্টেম এর মাঝে JSON ফরম্যাটে ডাটা এক্সচেঞ্জ করে, কেননা JSON ফরম্যাটে ডাটা এক্সচেঞ্জ করা খুবই সহজ।
JSON ডাটা নিয়ে কাজ করার জন্য Go এর “encoding/json” প্যাকেজের বেশ কিছু বিল্ট-ইন ফাংশন রয়েছে।
- json.marshal():
যে কোন ডাটা টাইপ এর ভ্যারিয়েবল কে জেসন ফরম্যাট এ এনকোড করে –
// Boolean values encode as JSON booleans
var boolean = true
bolB, _ := json.Marshal(boolean)
for i := range bolB {
fmt.Print(string(bolB[i]))
}
আউটপুট:
true
এই কোড থেকে বোঝা যাচ্ছে যে, boolean ভ্যালুটিকে কে json.Marshal() ব্যবহার করে আমরা byte টাইপের অ্যারেতে পরিবর্তন করছি, যেটার প্রতিটা এলিমেন্টকে আমরা string এ পরিবর্তন করে পরপর ক্যারেক্টারগুলো দেখতে পাচ্ছি। মূলতঃ এখানে, boolean ভ্যালু true থেকে string ভ্যালু “true” করা হয়েছে।
এরকম ভাবে আমরা int, ফ্লোট, string টাইপের ভ্যারিয়েবলকে []byte এ convert করতে পারি, যেটাকে বোঝার জন্য মূলতঃ string এ convert করা হচ্ছে।
intB, _ := json.Marshal(1)
fmt.Println(string(intB))
fltB, _ := json.Marshal(2.34)
fmt.Println(string(fltB))
strB, _ := json.Marshal("Gopher")
fmt.Println(string(strB))
আউটপুট:
1
2.34
"Gopher"
এখানের সব গুলা উদাহরণে Marshal() ফাংশন টা JSON এ পরিবর্তিত byte অ্যারে দিবে, নতুবা এরর দিবে (এখানে উদাহরণের সুবিধার্থে, ‘_’ দিয়ে তা ignore করা হয়েছে, কিন্তু বাস্তব জীবনে যেকোনো প্রজেক্ট করতে গেলে অবশ্যই এই এররগুলো চেক করা লাগবে।
কোনো অ্যারেকেও আমরা JSON ফরম্যাটে নিয়ে যেতে পারি –
slcD := []string{"apple", "peach", "pear"}
fmt.Println("before:", slcD)
slcB, _ := json.Marshal(slcD)
fmt.Println("after:",string(slcB))
এর আউটপুট –
before: [apple peach pear]
after: ["apple","peach","pear"]
কিন্তু আমরা যদি JSON এর কি-ভ্যালু ফরম্যাটে পেতে চাই, তাহলে আমাদের লাগবে map/struct –
mapD := map[string]int{"apple": 5, "lettuce": 7}
mapB, _ := json.Marshal(mapD)
fmt.Println(string(mapB))
এই কোডের আউটপুট –
{"apple":5,"lettuce":7}
এখানে Map এর ভিতরের সব এলিমেন্ট একই ডাটা টাইপের। আমরা যদি এমন JSON অবজেক্ট চাই যেখানে একেক ফিল্ড একেক টাইপের, তাহলে আমাদের Struct ইউজ করা লাগবে(অথবা map এর ভ্যালু হিসেবে ইন্টারফেস দিতে হবে)। এখানে একটি গুরুত্বপূর্ণ বিষয় হলোঃ Struct এর field গুলোর নাম অবশ্যই বড় হাতের অক্ষর দিয়ে শুরু হতে হবে যদি সেগুলো এক্সপোর্টেবল করতে চাই।–
type response1 struct {
Page int
Fruits []string
}
res1D := &response1{
Page: 1,
Fruits: []string{"apple", "peach", "pear"},
}
res1B, _ := json.Marshal(res1D)
fmt.Println(string(res1B))
আউটপুট –
{"Page":1,"Fruits":["apple","peach","pear"]}
আমরা চাইলে, JSON এর key গুলো কাস্টম করে নিতে পারি, এর জন্য Struct এর ফিল্ড গুলোর পাশে JSON ফরম্যাটে key এর নাম কি হবে তা “json” ট্যাগ দিয়ে বলে দিতে হবে। নিচের কোডটি খেয়াল করি –
type response2 struct {
Page int `json:"page-number"`
Fruits []string `json:"fruits-List"`
}
res1D := &response2{
Page: 1,
Fruits: []string{"apple", "peach", "pear"},
}
res1B, _ := json.Marshal(res1D)
fmt.Println(string(res1B))
JSON ফরম্যাটের key গুলো খেয়াল করি –
{"page-number":1,"fruits-List":["apple","peach","pear"]}
- json.Unmarshal():
এটা মূলত JSON ডাটা ডিকোড করতে ব্যবহার করা হয়, কিছু অতিরিক্ত নিয়মসহ, এটি Marshal() এর ঠিক উল্টো। আমাদের যে কোন একটা ভ্যারিয়েবল বলে দিতে হয়, যেখানে আমরা ডিকোডেড JSON অবজেক্ট কে রাখব।
Syntax –
func Unmarshal(data []byte, v any) error
যদি ভ্যারিয়েবল 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