طرح‌چه

جی‌سان (JSON) | گولنگ به زبان ساده

۲۰ مهر ۱۴۰۴

جی‌سان (JSON) | گولنگ به زبان ساده
گولنگ مانند باقی زبان های برنامه نویسی از JSON پشتیبانی می‌کند. در اینجا با نحوه استفاده از جی‌سان در برنامه های گولنگی آشنا میشیم.

جی‌سان (JSON) یا JavaScript Object Notation، یک فرمت ساده و قابل‌خواندن برای تبادل داده‌ها است. از لحاظِ ساختارِ ظاهری, داده ها در JSON شبیه اشیا و یا آرایه ها در جاوا اسکریپت هستند. تقریبا تمام زبان‌های برنامه نویسی از JSON پشتیبانی می‌کنند و گولنگ نیز یکی از این زبان ها است.

در گولنگ یکی از روش های کار با جی‌سان استفاده از پکیج استاندارد JSON است.

تبدیل داده به فرمت JSON

برای اینکد کردن داده (encode) به جی‌سان از تابع Marshal استفاده میکنیم. این تابع دارای یک پارامتر ورودی و دو پارامتر خروجی به شکل زیر است:

func Marshal(v interface{}) ([]byte, error)

در صورتی که خطایی رخ ندهد اولین پارامتر خروجی دارای داده با فرمت جی‌سان و مقدار error برابر با nil خواهد بود.

package main

import (
    "encoding/json"
    "log"
)

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
    m := Message{"Alice", "Hello", 1294706395881547000}

    b, err := json.Marshal(m)
    if err != nil {
        log.Println("error:", err)

        return
    }

    log.Println(string(b)) 
}

یکسری نکات در هنگام استفاده از این تابع وجود دارد:

  • چنل ها (channel) , اعداد مختلط (complex) و فانکشن ها را نمی‌توان به جی‌سان تبدیل کرد.

  • ساختارهای حلقوی (cyclic) پشتیبانی نمی‌شوند. استفاده از ساختارهای حلقوی باعث ایجاد یک حلقه بی‌نهایت در این تابع می‌شوند.

  • اشاره گرها (pointers) به مقادیری که به آن اشاره میکنند اینکد خواهند شد. یا اگر اشاره گر nil باشد به صورت null اینکد می‌شود.

  • تنها فیلد هایی در تابع Marshal و Unmarshal استفاده می‌شوند که قابل دسترسی باشند! یعنی scope آنها طوری باشد که بتوان به آن فیلدها دسترسی داشت.

در صورتی که از struct استفاده می‌کنیم, میتوانیم با استفاده از تگ‌ها مشخص کنیم که هر فیلد دقیقا با چه نامی در JSON نهایی اینکد (encode یا marshal) شود:

package main

import (
    "encoding/json"
    "log"
)

type Message struct {
    Name string `json:"first_name"`
    Body string `json:"payload"`
}

func main() {
    m := Message{"Alice", "Hello"}

    b, err := json.Marshal(m)
    if err != nil {
        log.Println("error:", err)

        return
    }

    log.Println(string(b)) 
}

در مثال بالا فیلد Name با نام first_name به JSON تبدیل می‌شود. همچنین فیلد Body با نام payload اینکد می‌شود. بدین صورت با استفاده از تگ ‍json میتوانیم نام فیلدها را دقیقا مشخص کنیم.

تبدیل JSON به فرمت قابل استفاده‌ در برنامه

برای دیکد کردن JSON از تابع Unmarshal استفاده میکنیم. پارامترهای این تابع به شکل زیر است:

func Unmarshal(data []byte, v interface{}) error

این تابع دارای دو پارامتر ورودی و یک مقدار خروجی است. اولین پارامتر ورودی اطلاعات با فرمت JSON هستند, دومین پارامتر باید یک اشاره گر به یک متغیر باشد. این تابع سعی میکند تا داده های پارامتر اول را که با فرمت JSON هستند درون متغیری که به عنوان پارامتر دوم مشخص کرده ایم ذخیره کند و در صورتی که کار موفقیت آمیز باشد مقدار error برابر با nil خواهد بود.

کاربرد این تابع به شکل زیر است:

package main

import (
    "encoding/json"
    "log"
)

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
    b := []byte(`{"Name":"Bob","Food":"Pickle"}`)

    var m Message
    if err := json.Unmarshal(b, &m); err != nil {
        log.Println("error:", err)

        return
    }

    log.Printf("%#v", m)
}

در مثال بالا اگر b حاوی JSON معتبری باشد که در m جا شود، پس از فراخوانی تابع Unmarshal مقدار err برابر با nil خواهد بود و داده‌های b در ساختار m ذخیره خواهند شد و در نهایت در خروجی استاندارد (STDOUT) چاپ می‌شوند.

تابع Unmarshal چگونه فیلد متناظر با داده های دیکد (decode یا unmarshal) شده را تشخیص می‌دهد؟

برای یک کلید JSON با نام «Foo»، تابع Unmarshal به ترتیب زیر در فیلدهای متغیری که به عنوان پارامتر دوم پاس داده ایم جست‌وجو می‌کند و در صورتی که تطبیق انجام شود اطلاعات را در آن فیلد ذخیره می‌کند:

  • ابتدا به دنبال فیلدی می‌گردد که قابل دسترسی باشد (scope) و دارای تگ (tag) با نام «Foo» باشد.

  • در صورت پیدا نکردن مورد بالا، به دنبال فیلدی می‌گردد که قابل دسترسی (scope) و دارای نام دقیق «Foo» باشد.

  • اگر همچنان موردی یافت نشود، به دنبال فیلدی می‌گردد که قابل دسترسی (scope) و نام آن با «Foo» به صورت غیرحساس به حروف بزرگ و کوچک (case-insensitive) مطابقت داشته باشد؛ مانند «FOO» یا «FoO».

در حالتی که ساختار JSON با دیتاتایپ متغیری که میخواهیم اطلاعات درون آن قرار گیرند منطبق نباشد, تابع Unmarshal تنها فیلدهایی را مقداردهی می‌کند که بتواند آن‌ها را شناسایی کند. در مثال زیر، فقط فیلد Name در متغیر m مقداردهی خواهد شد و فیلد Food نادیده گرفته می‌شود.

package main

import (
    "encoding/json"
    "log"
)

type Message struct {
    Name string
    Body string
    Time int64

	// unexported field
    password string
}

func main() {
    b := []byte(`{"Name":"Bob","Food":"Pickle"}`)

    var m Message
    if err := json.Unmarshal(b, &m); err != nil {
        log.Println("error:", err)
    }
}

این ویژگی زمانی بسیار مفید است که بخواهید از یک داده‌ی JSON بزرگ، تنها از چند فیلد خاص درون برنامه استفاده کنید. علاوه بر این، این رفتار نشان می‌دهد که در متغیر مقصد, فیلدهایی که قابل دسترسی نیستند (scope)، تحت تأثیر عملیات Unmarshal قرار نمی‌گیرند و مقدار آن‌ها بدون تغییر باقی می‌ماند.

در صورتی که از struct استفاده می کنیم, با استفاده از تگ گذاری فیلد ها میتوان مشخص کرد که اطلاعات در هنگام Marshal و Unmarshal درون چه فیلدی انکد (encode) و دیکد (decode) شوند.

package main

import (
    "encoding/json"
    "log"
)

type Message struct {
    Name string `json:"name"`
    Body string `json: "payload"`
}

func main() {
    b := []byte(`{"name":"Bob", "payload":"Pickle"}`)

    var m Message
    if err := json.Unmarshal(b, &m); err != nil {
        log.Printf("error:", err)
    }
}

دیکد کردن جی‌سان (JSON) دارای ساختار ناشناخته یا متغیر

بدون آن‌که ساختار داده‌ی JSON را از پیش بدانیم، می‌توانیم آن را با استفاده از تابع Unmarshal در یک متغیر از نوع interface{} دیکد (decode) کنیم.

package main

import (
    "encoding/json"
    "log"
)

func main() {
    b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

    var f interface{}
    if err := json.Unmarshal(b, &f); err != nil {
        log.Println("error:", err)

        return
    }

    log.Printf("%#v", f)
}

در مثال بالا اطلاعات مانند یک مپ (map) که کلیدهای آن از نوع string و مقادیر از نوع interface{} هستند درون متغیر f دیکد می شوند, چیزی همانند مثال زیر:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

در این حالت برای دسترسی به اطلاعات میتونیم با استفاده از interface assertion نوع ‍f را از interface{} به map[string]interface{} تبدیل کنیم و سپس به اطلاعات دسترسی داشته باشیم:

package main

import (
    "encoding/json"
    "log"
)

func main() {
    b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

    var f interface{}
    if err := json.Unmarshal(b, &f); err != nil {
        log.Println("error:", err)

        return
    }

    m := f.(map[string]interface{})

    for k, v := range m {
        switch vv := v.(type) {
        case string:
            fmt.Println(k, "is string", vv)
        case float64:
            fmt.Println(k, "is float64", vv)
        case []interface{}:
            fmt.Println(k, "is an array:")
            for i, u := range vv {
                fmt.Println(i, u)
            }
        default:
            fmt.Println(k, "is of a type I don't know how to handle")
        }
    }
}

پکیج json در زبان Go از نوع‌های map[string]interface{} و []interface{} برای نگهداری اشیاء (object) و آرایه‌ها (array) استفاده می‌کند. یعنی قادر است تا هر داده‌ی JSON معتبری را بدون مشکل درون یک متغیر از نوع interface{} دیکد (unmarshal) کند. در این حالت نوع‌های پیش‌فرضی که Go برای داده‌های JSON در نظر می‌گیرد به صورت لیست زیر هستند:

  • bool برای مقدارهای بولی (true / false)
  • float64 برای عددها
  • string برای رشته‌ها
  • nil برای مقدار null

با استفاده از لیست بالا ما در مثال قبل یک حلقه for نوشتیم که روی اطلاعاتی که دیکد شده است حرکت میکند و هرکدام از مقادیر را در خروجی استاندارد (stdout) چاپ می‌کند.

نوع داده‌ی رفرس (Reference Type)

در گولنگ یکسری دیتا تایپ وجود دارند که به صورت پیش فرض دارای مقدار nil هستند مگر اینکه به آنها حافظه تخصیص بدهیم. برای مثال نوع های map , slice و هر نوع داده ای که پوینتر (pointer) باشد از این دسته هستند.

تابع Unmarshal در هنگام ذخیره کردن اطلاعات درون نوع داده‌ی رفرنس, در صورتی که حافظه ای به نوع داده ها تخصیص نداده شده باشد, به آنها حافظه تخصیص می‌دهد.

package main

import (
    "encoding/json"
    "log"
)

type Command struct {
   Name string
}

type Message struct {
    Body string
    Time int64
}

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
    AvailableChannels []string
}

func main() {
    b := []byte(`{"cmd": {"name": "turn-on"}, "msg": {"body":"channel3"}}`)

    var f interface{}
    if err := json.Unmarshal(b, &f); err != nil {
        log.Println("error:", err)

        return
    }

    log.Printf("%#v", f)
}

در مثال بالا, تابع Unmarshal وقتی داده‌ی JSON رو به ساختار IncomingMessage تبدیل می‌کند، فقط به قسمت‌هایی از ساختار حافظه تخصیص میدهد (allocate memory) که واقعاً در داده‌ی JSON وجود دارند.

محتوای این پست برگرفته شده از پست اصلی در مورد JSON در سایت رسمی گولنگ است. برای اطلاعات تکمیلی‌تر می‌توانید راجب پکیج json و jsonrpc نیز مطالعه کنید.


قسمت قبل: Defer , Panic و Recover | گولنگ به زبان ساده

قسمت بعد: به زودی…


دیدگاه ها