Go ile çok yüksek ihtimal ağ servisleri geliştireceksiniz. Dış dünyadan veri
almak ya da dış dünyadan istek atan bir istemciye cevap vermek için bir kaç
farklı yöntem mevcut. Bunlardan en sık kullanacaklarınız arasında json
yani
hafif siklet veri değişim formatını kullancaksınız. Bu bakımdan;
- Ya,
go
daki bir tipi, dış dünyanın anlayacağıjson
hale (Marshal
) - Ya da dış dünyadan
json
formatında gelen veriyigo
’nun anlayacağı hale (Unmarshal
)
getirmek çok sık yaptığımız bir iş olacak. Bu dönüşüm esnasında
Elimizdeki go
tipini []byte
haline dönüştürme işlemidir:
https://go.dev/play/p/7yav9dmBRb7
package main
import (
"encoding/json"
"fmt"
"log"
)
// UserLevel is a custom type definition uses string for defining user level.
type UserLevel string
// UserLevels holds collection of UserLevel types.
type UserLevels []UserLevel
func main() {
UserLevelAdmin := UserLevel("admin")
UserLevelModerator := UserLevel("moderator")
UserLevelAnonymous := UserLevel("anonymous")
userLevels := UserLevels{
UserLevelAdmin,
UserLevelModerator,
UserLevelAnonymous,
}
j, err := json.Marshal(userLevels)
if err != nil {
log.Fatal(err)
}
fmt.Println(j)
// [91 34 97 100 109 105 110 34 44 34 109 111 100 101 114 97 116 111 114 34 44 34 97 110 111 110 121 109 111 117 115 34 93]
fmt.Println(string(j))
// ["admin","moderator","anonymous"]
}
j
’nin string görüntüsü: ["admin","moderator","anonymous"]
şeklindedir. Bu
aslında JavaScript’teki array
tipidir.
Şimdi, tam ters işlemi yapalım; dış dünyadan bize ["admin","moderator","anonymous"]
gelsin:
https://go.dev/play/p/E6L7GhKpJmu
package main
import (
"encoding/json"
"fmt"
"log"
)
// UserLevel is a custom type definition uses string.
type UserLevel string
// UserLevels holds collection of UserLevel types.
type UserLevels []UserLevel
func main() {
// byte slice from raw string.
input := []byte(`["admin","moderator","anonymous"]`)
// target data type for serialization.
var userLevels UserLevels
if err := json.Unmarshal(input, &userLevels); err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", userLevels)
// main.UserLevels{"admin", "moderator", "anonymous"}
}
Genelde bu çevirme işlerini struct
tipi üzerinde yaparız. Dışarıdan gelen ya
da dışarıya gidecek veride daha yapısal (structural) veri kullanmak isteriz:
https://go.dev/play/p/BB5D_VrLpLa
package main
import (
"encoding/json"
"fmt"
"log"
)
// User represents user model.
type User struct {
Name string
Email string
Age int
}
func main() {
u := User{
Name: "Uğur Özyılmazel",
Email: "[email protected]",
Age: 51,
}
j, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(j)
// [123 34 78 97 109 101 34 58 34 85 196 159 117 114 32 195 150 122 121 196 177 108 109 97 122 101 108 34 44 34 69 109 97 105 108 34 58 34 118 105 103 111 64 101 120 97 109 112 108 101 46 99 111 109 34 44 34 65 103 101 34 58 53 49 125]
fmt.Println(string(j))
// {"Name":"Uğur Özyılmazel","Email":"[email protected]","Age":51}
}
Dikkat ettiyseniz, []byte
’a baktığımızda; {"Name":"Uğur Özyılmazel","Email":"[email protected]","Age":51}
alan adlarının struct
field name’leri ile aynı olduğunu görürüz. json
convention’a baktığımızda
alan adlarının bir kuralı var. Ana kural key
’lerin (Name, Email ...) mutlaka
Unicode karakterlerden oluşmasıdır. Kimileri camelCase
kimileri
snake_case
de kullanabilir.
Kuralına uygun yazılmış bir rest-api servisi, alan adı kuralı olarak
snake_case
kullanmalıdır.
Peki, biz Name
yerine name
nasıl döneceğiz? Bu durumda json:"FIELD"
tag
devreye girer:
https://go.dev/play/p/EY_4u3pniOf
package main
import (
"encoding/json"
"fmt"
"log"
)
// User represents user model.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
u := User{
Name: "Uğur Özyılmazel",
Email: "[email protected]",
Age: 51,
}
j, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(j)
// [123 34 110 97 109 101 34 58 34 85 196 159 117 114 32 195 150 122 121 196 177 108 109 97 122 101 108 34 44 34 101 109 97 105 108 34 58 34 118 105 103 111 64 101 120 97 109 112 108 101 46 99 111 109 34 44 34 97 103 101 34 58 53 49 125]
fmt.Println(string(j))
// {"name":"Uğur Özyılmazel","email":"[email protected]","age":51}
}
json
tag’i (aslında bu bir struct annotation) sadece bu işe yaramaz;
// JSON çıktıda key "myName" olarak görünür.
Field int `json:"myName"`
// JSON çıktıda key "myName" olarak görünür fakat değer "empty" (zero-value) ise
// omit edilir (yani dışarıda bırakılır) ve bu field çıktıda yer almaz!
Field int `json:"myName,omitempty"`
// JSON çıktıda key "Field" olarak görünür fakat değer "empty" (zero-value) ise
// omit edilir (yani dışarıda bırakılır) ve bu field çıktıda yer almaz!
Field int `json:",omitempty"`
// Field komple görmezden gelinir (ignore) ve çıktıda yer almaz!
Field int `json:"-"`
// JSON çıktıda key "-" olarak görünür.
Field int `json:"-,"`
Ek olarak, dış dünyadan gelen veri string
görünümünde ama değer olarak
integer
;
{"age": "51"}
age
’in tip olarak değeri 51
yani string
ama biz bunu içeride int
olarak kullanmak istiyoruz (bazı kötü tasarlanmış api’larda bu tür durumlar oluyor):
Age int `json:"age,string"`
şeklinde de kullanabiliyoruz:
https://go.dev/play/p/Qp0lnPOPBRQ
package main
import (
"encoding/json"
"fmt"
"log"
)
// User represents user model.
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Age int `json:"age,string,omitempty"`
Password string `json:"-"`
}
func main() {
u := User{
Name: "Uğur Özyılmazel",
Email: "[email protected]",
Age: 51,
}
j, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
fmt.Println(j)
// [123 34 110 97 109 101 34 58 34 85 196 159 117 114 32 195 150 122 121 196 177 108 109 97 122 101 108 34 44 34 101 109 97 105 108 34 58 34 118 105 103 111 64 101 120 97 109 112 108 101 46 99 111 109 34 44 34 97 103 101 34 58 34 53 49 34 125]
fmt.Println(string(j))
// {"name":"Uğur Özyılmazel","email":"[email protected]","age":"51"}
u2 := User{
Name: "Uğur Özyılmazel",
}
j, err = json.Marshal(u2)
if err != nil {
log.Fatal(err)
}
fmt.Println(j)
// [123 34 110 97 109 101 34 58 34 85 196 159 117 114 32 195 150 122 121 196 177 108 109 97 122 101 108 34 125]
fmt.Println(string(j))
// {"name":"Uğur Özyılmazel"}
}
Şimdi örneğe bakalım, struct
alanları içinde gezen, tag’leri bulan ve özelleştirilmiş time
kullanan bir
yapı bulunuyor. go
, Marshal
işlemi yaparken, eğer custom type varsa ve bu
type’ın MarshalJSON()
metotu varsa onu kullanıyor. describeStruct
ise
reflect
paketini kullanarak struct içinde geziyor.
https://go.dev/play/p/7ny1yu09idp
package main
import (
"encoding/json"
"fmt"
"log"
"reflect"
"time"
)
// User holds user model data. User struct has json tags!
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
BirthDate time.Time `json:"birth_date"`
Admin bool `json:"admin"`
LastVisit time.Time `json:"-"` // omitted, ignore
}
const customTimeLayout = "2006-01-02T15:04:05-07:00"
// CustomTime is a custom type definition uses time.Time, uses custom marshal format.
type CustomTime struct {
time.Time
}
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return []byte(`"` + ct.Time.Format(customTimeLayout) + `"`), nil
}
// UserWithCustomTime holds user model data with custom time type!
type UserWithCustomTime struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
BirthDate time.Time `json:"birth_date"`
Admin bool `json:"admin"`
LastVisit CustomTime `json:",omitempty"`
}
// OtherUser holds user model data, only one field has tag!
type OtherUser struct {
FirstName string
LastName string
Age int
BirthDate time.Time
Admin bool
LastVisit *time.Time `json:",omitempty"` // this field uses pointer, can be nil, can be omitted if unset!
}
func describeStruct(v any) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf(
"Name: %-12s Type: %-18s Tag: %-20s json: %s\n",
field.Name,
field.Type,
field.Tag,
field.Tag.Get("json"),
)
}
fmt.Println()
}
func marshalStruct(v any) error {
j, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
fmt.Printf("%s\n\n", j)
return nil
}
func main() {
trTZ := time.FixedZone("UTC+3", +3*60*60)
now := time.Now()
u1 := User{
FirstName: "Uğur",
LastName: "Özy",
Age: 49,
BirthDate: time.Date(1972, time.August, 13, 10, 0, 0, 0, trTZ),
Admin: true,
LastVisit: now,
}
describeStruct(u1)
// Name: FirstName Type: string Tag: json:"first_name" json: first_name
// Name: LastName Type: string Tag: json:"last_name" json: last_name
// Name: Age Type: int Tag: json:"age" json: age
// Name: BirthDate Type: time.Time Tag: json:"birth_date" json: birth_date
// Name: Admin Type: bool Tag: json:"admin" json: admin
// Name: LastVisit Type: time.Time Tag: json:"-" json: -
u2 := OtherUser{
FirstName: "Ezel",
LastName: "Özy",
Age: 10,
BirthDate: time.Date(2011, time.August, 13, 7, 0, 0, 0, trTZ),
// we don't have LastVisit field!
}
describeStruct(u2)
// Name: FirstName Type: string Tag: json:
// Name: LastName Type: string Tag: json:
// Name: Age Type: int Tag: json:
// Name: BirthDate Type: time.Time Tag: json:
// Name: Admin Type: bool Tag: json:
// Name: LastVisit Type: *time.Time Tag: json:",omitempty" json: ,omitempty
u3 := OtherUser{
FirstName: "Ezel",
LastName: "Özy",
Age: 12,
BirthDate: time.Date(2011, time.August, 13, 7, 0, 0, 0, trTZ),
LastVisit: &now,
}
describeStruct(u3)
// Name: FirstName Type: string Tag: json:
// Name: LastName Type: string Tag: json:
// Name: Age Type: int Tag: json:
// Name: BirthDate Type: time.Time Tag: json:
// Name: Admin Type: bool Tag: json:
// Name: LastVisit Type: *time.Time Tag: json:",omitempty" json: ,omitempty
u4 := UserWithCustomTime{
FirstName: "Ezel",
LastName: "Özyılmazel",
Age: 12,
BirthDate: time.Date(2011, time.August, 13, 7, 0, 0, 0, trTZ),
LastVisit: CustomTime{now},
}
describeStruct(u4)
// Name: FirstName Type: string Tag: json:"first_name" json: first_name
// Name: LastName Type: string Tag: json:"last_name" json: last_name
// Name: Age Type: int Tag: json:"age" json: age
// Name: BirthDate Type: time.Time Tag: json:"birth_date" json: birth_date
// Name: Admin Type: bool Tag: json:"admin" json: admin
// Name: LastVisit Type: main.CustomTime Tag: json:",omitempty" json: ,omitempty
if err := marshalStruct(u1); err != nil {
log.Fatal(err)
}
// {
// "first_name": "Uğur",
// "last_name": "Özy",
// "age": 49,
// "birth_date": "1972-08-13T10:00:00+03:00",
// "admin": true
// }
if err := marshalStruct(u2); err != nil {
log.Fatal(err)
}
// {
// "FirstName": "Ezel",
// "LastName": "Özy",
// "Age": 10,
// "BirthDate": "2011-08-13T07:00:00+03:00",
// "Admin": false
// }
if err := marshalStruct(u3); err != nil {
log.Fatal(err)
}
// {
// "FirstName": "Ezel",
// "LastName": "Özy",
// "Age": 12,
// "BirthDate": "2011-08-13T07:00:00+03:00",
// "Admin": false,
// "LastVisit": "2023-08-13T14:07:09.537756+03:00"
// }
if err := marshalStruct(u4); err != nil {
log.Fatal(err)
}
// {
// "first_name": "Ezel",
// "last_name": "Özyılmazel",
// "age": 12,
// "birth_date": "2011-08-13T07:00:00+03:00",
// "admin": false,
// "LastVisit": "2023-08-13T14:07:09+03:00"
// }
}
Aslında pretty print
yaptığımız kısımda marshalStruct(v any)
fonksiyonu
içinde kullanmıştık, ek olarak html karakterlerini de escape etmeyi görelim;
https://go.dev/play/p/PaLSl8ya-ql
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
data := map[string]any{
"message": `this is <strong>bold</strong> text`,
}
buffer := new(bytes.Buffer) // create buffer, returns pointer.
encoder := json.NewEncoder(buffer) // point to buffer
encoder.SetEscapeHTML(true) // enable html escape
encoder.SetIndent("", " ") // indent 4 spaces each key
// serialize data to buffer
if err := encoder.Encode(data); err != nil {
log.Fatal(err)
}
fmt.Println(buffer)
// fmt.Println(buffer.String())
// fmt.Printf("%s\n", buffer)
// {
// "message": "this is \u003cstrong\u003ebold\u003c/strong\u003e text"
// }
buffer = new(bytes.Buffer) // create buffer, returns pointer.
encoder = json.NewEncoder(buffer) // point to buffer
encoder.SetEscapeHTML(false) // disable html escape
encoder.SetIndent("", " ") // indent 4 spaces each key
// serialize data to buffer
if err := encoder.Encode(data); err != nil {
log.Fatal(err)
}
fmt.Println(buffer)
// {
// "message": "this is <strong>bold</strong> text"
// }
}
Dış dünyadan gelen veriyi go
tipine Unmarshal
ile çeviriyorduk. Bazen bu
işi biraz daha kontrollü yapmak gerekebilir. Bu durumda json.NewDecoder
kullanırız. Beklenmeyen bir alan geldiğinde hata yakalama ile bunu yakalayıp
istersek işlemi ilerletmeyebiliriz.
Şimdi örneğe bakalım;
https://go.dev/play/p/hCGfrUGRn5-
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"time"
)
// User holds user model data. User struct has json tags!
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
BirthDate time.Time `json:"birth_date"`
Admin bool `json:"admin"`
LastVisit *time.Time `json:"last_visit"`
}
// Users holds user slice of Users.
type Users []User
func main() {
input := `[
{
"first_name": "Uğur",
"last_name": "Özyılmazel",
"age": 51,
"birth_date": "1972-08-13T09:00:00.0+03:00"
},
{
"first_name": "Ömer",
"last_name": "Özyılmazel",
"age": 41,
"birth_date": "1982-08-13T14:00:00.0+03:00",
"admin": true
},
{
"this": "This",
"is": "is",
"fake": "fake"
}
]`
b := []byte(input)
d := json.NewDecoder(bytes.NewReader(b))
// accepts io.Reader, convert byte slice -> io.Reader satisfier
d.DisallowUnknownFields()
// raise error for unknown fields
var users Users
// if err := json.Unmarshal(b, &users); err != nil {
// log.Fatal(err)
// }
if err := d.Decode(&users); err != nil {
fmt.Println(err) // json: unknown field "this"
}
fmt.Printf("%+v\n", users)
// [{FirstName:Uğur LastName:Özyılmazel Age:51 BirthDate:1972-08-13 09:00:00 +0300 +0300 Admin:false LastVisit:<nil>} {FirstName:Ömer LastName:Özyılmazel Age:41 BirthDate:1982-08-13 14:00:00 +0300 +03 Admin:true LastVisit:<nil>} {FirstName: LastName: Age:0 BirthDate:0001-01-01 00:00:00 +0000 UTC Admin:false LastVisit:<nil>}]
for _, user := range users {
fmt.Printf("%+v\n", user)
fmt.Println(user.LastVisit, user.LastVisit == nil)
}
// {FirstName:Uğur LastName:Özyılmazel Age:51 BirthDate:1972-08-13 09:00:00 +0300 +0300 Admin:false LastVisit:<nil>}
// <nil> true
// {FirstName:Ömer LastName:Özyılmazel Age:41 BirthDate:1982-08-13 14:00:00 +0300 +03 Admin:true LastVisit:<nil>}
// <nil> true
// {FirstName: LastName: Age:0 BirthDate:0001-01-01 00:00:00 +0000 UTC Admin:false LastVisit:<nil>}
// <nil> true
j, err := json.MarshalIndent(users, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n\n", j)
// [
// {
// "first_name": "Uğur",
// "last_name": "Özyılmazel",
// "age": 51,
// "birth_date": "1972-08-13T09:00:00+03:00",
// "admin": false,
// "last_visit": null
// },
// {
// "first_name": "Ömer",
// "last_name": "Özyılmazel",
// "age": 41,
// "birth_date": "1982-08-13T14:00:00+03:00",
// "admin": true,
// "last_visit": null
// },
// {
// "first_name": "",
// "last_name": "",
// "age": 0,
// "birth_date": "0001-01-01T00:00:00Z",
// "admin": false,
// "last_visit": null
// }
// ]
}
Son alan; "this": "This",
ile başlayan kısım işlenmedi ama user
’ın son
değeri User
struct’ının zero-value (empty)’larıyla doldu. Yani bu decode
işleminde 3 tane elemanı olan bir slice çıktı.
Elimizde raw string olduğunda bunu map[string]any
’e cast ederek, içindeki
diğer verilere de ulaşabiliriz.
Eğer bazı alanların opsiyonel olmasını istiyorsak, özellikle struct
’a
Unmarshal
ederken, struct içine bir tane joker alan koyup bunu
map[string]any
yapıyoruz ve eksik alanları bu map’ten siliyoruz:
https://go.dev/play/p/u1mm5nr-jJx
package main
import (
"encoding/json"
"fmt"
"log"
)
// User holds user data
type User struct {
Name string `json:"name"`
Email string
Age int
Optionals map[string]any `json:"-"`
}
func main() {
incoming := `{
"name": "Uğur",
"email": "[email protected]",
"age": 51,
"foo": 1,
"bar": "2"
}`
u := User{}
if err := json.Unmarshal([]byte(incoming), &u.Optionals); err != nil {
log.Fatal(err)
}
fmt.Println("u.Optionals", u.Optionals)
// u.Optionals map[age:51 bar:2 email:[email protected] foo:1 name:Uğur]
if v, ok := u.Optionals["name"].(string); ok {
u.Name = string(v)
delete(u.Optionals, "name")
}
if v, ok := u.Optionals["email"].(string); ok {
u.Email = string(v)
delete(u.Optionals, "email")
}
if v, ok := u.Optionals["age"].(float64); ok {
u.Age = int(v)
delete(u.Optionals, "age")
}
fmt.Printf("u: %+v\n", u)
// u: {Name:Uğur Email:[email protected] Age:51 Optionals:map[bar:2 foo:1]}
if u.Email != "" {
fmt.Println("u.Email", u.Email)
// u.Email [email protected]
}
if u.Age != 0 {
fmt.Println("u.Age", u.Age)
// u.Age 51
}
if len(u.Optionals) > 0 {
fmt.Printf("you have %d invalid field(s)\n", len(u.Optionals))
// you have 2 invalid field(s)
for v := range u.Optionals {
fmt.Printf("%q\n", v)
}
// "foo"
// "bar"
}
}
Unmarshal
işlemine başlamadan önce, gelen verinin json’a uygun olup
olmadığını doğrulamak için json.Valid
kullanıyoruz;
https://go.dev/play/p/Z0a7mVQpXoU
package main
import (
"encoding/json"
"fmt"
)
func main() {
goodJSON := `{"example": 1}`
badJSON := `{"example":2:]}}`
fmt.Println("goodJSON is valid?", json.Valid([]byte(goodJSON)))
// goodJSON is valid? true
fmt.Println("badJSON is valid?", json.Valid([]byte(badJSON)))
// badJSON is valid? false
}
// goodJSON is valid? true
// badJSON is valid? false
Uygulananız başka bir servise rest-api üzerinden json
ile veri çekiyor fakat
gelen veri json array şeklinde değil;
https://go.dev/play/p/H95BTncScTb
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"strings"
)
// Person represents person model.
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
// People represents collection on Person slice.
type People []Person
func main() {
// bulk data, looks like json array?
incoming := `
{"name": "Fred", "age": 40}
{"name": "Mary", "age": 21}
{"name": "Pat", "age": 30}
`
decoder := json.NewDecoder(strings.NewReader(incoming))
var b bytes.Buffer
encoder := json.NewEncoder(&b)
var p Person
var people People
for decoder.More() {
if err := decoder.Decode(&p); err != nil {
log.Print("decode err", err)
continue
}
fmt.Printf("p: %+v\n", p)
// p: {Name:Fred Age:40}
// p: {Name:Mary Age:21}
// p: {Name:Pat Age:30}
people = append(people, p)
if err := encoder.Encode(p); err != nil {
log.Panic("encode err", err) // do not panic!
}
}
fmt.Println(b.Bytes())
// [123 34 110 97 109 101 34 58 34 70 114 101 100 34 44 34 97 103 101 34 58 52 48 125 10 123 34 110 97 109 101 34 58 34 77 97 114 121 34 44 34 97 103 101 34 58 50 49 125 10 123 34 110 97 109 101 34 58 34 80 97 116 34 44 34 97 103 101 34 58 51 48 125 10]
// fmt.Println(string(b.Bytes()))
fmt.Println(b.String())
// {"name":"Fred","age":40}
// {"name":"Mary","age":21}
// {"name":"Pat","age":30}
fmt.Printf("people: %+v\n", people)
// people: [{Name:Fred Age:40} {Name:Mary Age:21} {Name:Pat Age:30}]
j, _ := json.MarshalIndent(people, "", " ")
fmt.Printf("%s\n", j)
// [
// {
// "name": "Fred",
// "age": 40
// },
// {
// "name": "Mary",
// "age": 21
// },
// {
// "name": "Pat",
// "age": 30
// }
// ]
}