diff --git a/pkg/json/json.go b/pkg/json/json.go index 470c65b..f9c16aa 100644 --- a/pkg/json/json.go +++ b/pkg/json/json.go @@ -1,7 +1,6 @@ package json import ( - "reflect" "sync" "github.com/bytedance/sonic" @@ -21,9 +20,7 @@ var ( // // It's like json.Marshal, but with some differences: // -// - It's ~4-5x faster in most cases. -// -// - It also supports custom Marshaler interface (MarshalJSONTo(buf []byte) []byte) +// - It supports custom Marshaler interface (MarshalJSONTo(buf []byte) []byte) // to allow further optimizations. // // - It leverages the strutils library. @@ -36,20 +33,20 @@ var ( // // `use_marshaler` to force using the custom marshaler for primitive types declaration (e.g. `type Status int`). // -// - It correct the behavior of *url.URL and time.Duration. +// - It corrects the behavior of *url.URL and time.Duration. // // - It does not support maps other than string-keyed maps. func Marshal(v any) ([]byte, error) { buf := newBytes() defer putBytes(buf) - return cloneBytes(appendMarshal(reflect.ValueOf(v), buf)), nil + return cloneBytes(appendMarshalAny(v, buf)), nil } func MarshalTo(v any, buf []byte) []byte { - return appendMarshal(reflect.ValueOf(v), buf) + return appendMarshalAny(v, buf) } -const bufSize = 1024 +const bufSize = 8192 var bytesPool = sync.Pool{ New: func() any { diff --git a/pkg/json/marshal.go b/pkg/json/marshal.go index a22f9fd..cae45d2 100644 --- a/pkg/json/marshal.go +++ b/pkg/json/marshal.go @@ -20,7 +20,6 @@ var ( marshalFuncByKind map[reflect.Kind]marshalFunc marshalFuncsByType = newCacheMap[reflect.Type, marshalFunc]() - flattenFieldsCache = newCacheMap[reflect.Type, []*field]() nilValue = reflect.ValueOf(nil) ) @@ -44,6 +43,7 @@ func init() { reflect.Map: appendMap, reflect.Slice: appendArray, reflect.Array: appendArray, + reflect.Struct: appendStruct, reflect.Pointer: appendPtrInterface, reflect.Interface: appendPtrInterface, } @@ -69,17 +69,15 @@ func must(buf []byte, err error) []byte { return buf } +func appendMarshalAny(v any, buf []byte) []byte { + return appendMarshal(reflect.ValueOf(v), buf) +} + func appendMarshal(v reflect.Value, buf []byte) []byte { if v == nilValue { return append(buf, "null"...) } kind := v.Kind() - if kind == reflect.Struct { - if res, ok := appendWithCachedFunc(v, buf); ok { - return res - } - return appendStruct(v, buf) - } marshalFunc, ok := marshalFuncByKind[kind] if !ok { panic(fmt.Errorf("unsupported type: %s", v.Type())) @@ -87,6 +85,10 @@ func appendMarshal(v reflect.Value, buf []byte) []byte { return marshalFunc(v, buf) } +func cacheMarshalFunc(t reflect.Type, marshalFunc marshalFunc) { + marshalFuncsByType.Store(t, marshalFunc) +} + func appendWithCachedFunc(v reflect.Value, buf []byte) (res []byte, ok bool) { marshalFunc, ok := marshalFuncsByType.Load(v.Type()) if ok { @@ -108,7 +110,7 @@ func appendUint(v reflect.Value, buf []byte) []byte { } func appendFloat(v reflect.Value, buf []byte) []byte { - return strconv.AppendFloat(buf, v.Float(), 'f', 2, 64) + return strconv.AppendFloat(buf, v.Float(), 'f', -1, 64) } func appendWithCustomMarshaler(v reflect.Value, buf []byte) (res []byte, ok bool) { @@ -154,40 +156,6 @@ func appendKV(k reflect.Value, v reflect.Value, buf []byte) []byte { return appendMarshal(v, buf) } -func appendStruct(v reflect.Value, buf []byte) []byte { - if res, ok := appendWithCustomMarshaler(v, buf); ok { - return res - } - buf = append(buf, '{') - oldN := len(buf) - fields := flattenFields(v.Type()) - - for _, f := range fields { - cur := v.Field(f.index) - if f.omitEmpty && f.checkEmpty(cur) { - continue - } - if !f.hasInner { - buf = f.appendKV(cur, buf) - buf = append(buf, ',') - } else { - if f.isPtr { - cur = cur.Elem() - } - for _, inner := range f.inner { - buf = inner.appendKV(cur.Field(inner.index), buf) - buf = append(buf, ',') - } - } - } - - n := len(buf) - if oldN != n { - buf = buf[:n-1] - } - return append(buf, '}') -} - func appendMap(v reflect.Value, buf []byte) []byte { if v.Type().Key().Kind() != reflect.String { panic(fmt.Errorf("map key must be string: %s", v.Type())) @@ -230,10 +198,6 @@ func appendArray(v reflect.Value, buf []byte) []byte { return append(buf, ']') } -func cacheMarshalFunc(t reflect.Type, marshalFunc marshalFunc) { - marshalFuncsByType.Store(t, marshalFunc) -} - func appendPtrInterface(v reflect.Value, buf []byte) []byte { return appendMarshal(v.Elem(), buf) } diff --git a/pkg/json/marshal_test.go b/pkg/json/marshal_test.go index 02d1599..088b98d 100644 --- a/pkg/json/marshal_test.go +++ b/pkg/json/marshal_test.go @@ -3,23 +3,17 @@ package json_test import ( stdJSON "encoding/json" "fmt" - "maps" "reflect" - "runtime/debug" "strconv" "testing" "github.com/bytedance/sonic" "github.com/stretchr/testify/require" "github.com/yusing/go-proxy/internal/utils/strutils" + . "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/pkg/json" ) -func init() { - debug.SetMemoryLimit(1024 * 1024) - debug.SetMaxStack(1024 * 1024) -} - type testStruct struct { Name string `json:"name"` Age int `json:"age"` @@ -104,19 +98,6 @@ type selfReferencing struct { Self *selfReferencing `json:"self"` } -var testData = map[string]any{ - "string": "test string", - "number": 42, - "float": 3.14159, - "bool": true, - "null_value": nil, - "array": []any{1, "2", 3.3, true, false, nil}, - "object": map[string]any{ - "nested": "value", - "count": 10, - }, -} - func TestMarshal(t *testing.T) { tests := []struct { name string @@ -386,8 +367,19 @@ func TestMarshal(t *testing.T) { input: map[string]testStruct{"one": {Name: "John", Age: 30, Score: 8.5}, "two": {Name: "Jane", Age: 25, Score: 9.5}}, }, { - name: "complex_map", - input: testData, + name: "complex_map", + input: map[string]any{ + "string": "test string", + "number": 42, + "float": 3.14159, + "bool": true, + "null_value": nil, + "array": []any{1, "2", 3.3, true, false, nil}, + "object": map[string]any{ + "nested": "value", + "count": 10, + }, + }, }, } @@ -411,34 +403,6 @@ func TestMarshal(t *testing.T) { } } -func TestMapAndMapSlice(t *testing.T) { - tests := []struct { - name string - input any - expected string - }{ - { - name: "Map", - input: Map[string]{"key1": "value1", "key2": "value2"}, - expected: `{"key1":"value1","key2":"value2"}`, - }, - { - name: "MapSlice", - input: MapSlice[string]{{"key1": "value1"}, {"key2": "value2"}}, - expected: `[{"key1":"value1"},{"key2":"value2"}]`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, _ := Marshal(tt.input) - if string(result) != tt.expected { - t.Errorf("Marshal(%v) = %s, want %s", tt.input, string(result), tt.expected) - } - }) - } -} - func TestMarshalSyntacticEquivalence(t *testing.T) { testData := []any{ "test\r\nstring", @@ -482,7 +446,46 @@ func TestMarshalSyntacticEquivalence(t *testing.T) { } } -func BenchmarkMarshalNoStructStdLib(b *testing.B) { +func TestWithTestStruct(t *testing.T) { + var custom, stdlib []byte + var err error + + custom, err = Marshal(TwitterObject) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + + stdlib, err = stdJSON.Marshal(TwitterObject) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + + var unmarshalCustom, unmarshalStdlib any + if err := Unmarshal(custom, &unmarshalCustom); err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + if err := sonic.Unmarshal(stdlib, &unmarshalStdlib); err != nil { + t.Fatalf("Unmarshal error: %v", err) + } + + ExpectEqual(t, unmarshalCustom, unmarshalStdlib) +} + +func BenchmarkMarshalSimpleStdLib(b *testing.B) { + testData := map[string]any{ + "string": "test string", + "number": 42, + "float": 3.14159, + "bool": true, + "null_value": nil, + "bytes": []byte("test"), + "array": []any{1, "2", 3.3, true, false, nil}, + "object": map[string]any{ + "nested": "value", + "count": 10, + }, + } + b.Run("StdLib", func(b *testing.B) { for b.Loop() { _, _ = stdJSON.Marshal(testData) @@ -502,28 +505,22 @@ func BenchmarkMarshalNoStructStdLib(b *testing.B) { }) } -func BenchmarkMarshalStruct(b *testing.B) { - withStruct := maps.Clone(testData) - withStruct["struct1"] = withAnonymous{Anonymous: Anonymous{Value: "one", Value2: 1}} - withStruct["struct2"] = &withPointerAnonymous{Anonymous: &Anonymous{Value: "two", Value2: 2}} - withStruct["struct3"] = &testStruct{Name: "three", Age: 30, Score: 9.8} - b.ResetTimer() - +func BenchmarkMarshalTestStruct(b *testing.B) { b.Run("StdLib", func(b *testing.B) { for b.Loop() { - _, _ = stdJSON.Marshal(withStruct) + _, _ = stdJSON.Marshal(TwitterObject) } }) b.Run("Sonic", func(b *testing.B) { for b.Loop() { - _, _ = sonic.Marshal(withStruct) + _, _ = sonic.Marshal(TwitterObject) } }) b.Run("Custom", func(b *testing.B) { for b.Loop() { - _, _ = Marshal(withStruct) + _, _ = Marshal(TwitterObject) } }) } diff --git a/pkg/json/struct.go b/pkg/json/struct.go index eb066e0..f8930ff 100644 --- a/pkg/json/struct.go +++ b/pkg/json/struct.go @@ -20,6 +20,10 @@ type field struct { marshal marshalFunc } +func (f *field) appendKV(v reflect.Value, buf []byte) []byte { + return f.marshal(v, append(buf, f.quotedNameWithCol...)) +} + const ( tagOmitEmpty = "omitempty" tagString = "string" // https://pkg.go.dev/github.com/yusing/go-proxy/pkg/json#Marshal @@ -28,13 +32,56 @@ const ( tagUseMarshaler = "use_marshaler" ) -func flattenFields(t reflect.Type) []*field { - fields, ok := flattenFieldsCache.Load(t) - if ok { - return fields +func appendStruct(v reflect.Value, buf []byte) []byte { + if res, ok := appendWithCachedFunc(v, buf); ok { + return res } - fields = make([]*field, 0, t.NumField()) + if res, ok := appendWithCustomMarshaler(v, buf); ok { + return res + } + + t := v.Type() + fields := flattenFields(t) + marshalFn := func(v reflect.Value, buf []byte) []byte { + return appendFields(v, fields, buf) + } + cacheMarshalFunc(t, marshalFn) + return marshalFn(v, buf) +} + +func appendFields(v reflect.Value, fields []*field, buf []byte) []byte { + buf = append(buf, '{') + oldN := len(buf) + + for _, f := range fields { + cur := v.Field(f.index) + if f.omitEmpty && f.checkEmpty(cur) { + continue + } + if !f.hasInner { + buf = f.appendKV(cur, buf) + buf = append(buf, ',') + } else { + if f.isPtr { + cur = cur.Elem() + } + for _, inner := range f.inner { + buf = inner.appendKV(cur.Field(inner.index), buf) + buf = append(buf, ',') + } + } + } + + n := len(buf) + if oldN != n { + buf = buf[:n-1] + } + return append(buf, '}') +} + +func flattenFields(t reflect.Type) []*field { + fields := make([]*field, 0, t.NumField()) for i := range t.NumField() { structField := t.Field(i) if !structField.IsExported() { @@ -95,11 +142,5 @@ func flattenFields(t reflect.Type) []*field { } f.quotedNameWithCol = strconv.Quote(f.quotedNameWithCol) + ":" } - - flattenFieldsCache.Store(t, fields) return fields } - -func (f *field) appendKV(v reflect.Value, buf []byte) []byte { - return f.marshal(v, append(buf, f.quotedNameWithCol...)) -} diff --git a/pkg/json/testdata_test.go b/pkg/json/testdata_test.go new file mode 100644 index 0000000..c1a160a --- /dev/null +++ b/pkg/json/testdata_test.go @@ -0,0 +1,563 @@ +/* + * Copyright 2021 ByteDance Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// from https://github.com/bytedance/sonic/blob/main/encoder/testdata_test.go +package json_test + +import "github.com/bytedance/sonic" + +var TwitterObject = func() *TwitterStruct { + var t TwitterStruct + err := sonic.Unmarshal([]byte(TwitterJson), &t) + if err != nil { + panic(err) + } + return &t +}() + +const TwitterJson = `{ + "statuses": [ + { + "coordinates": null, + "favorited": false, + "truncated": false, + "created_at": "Mon Sep 24 03:35:21 +0000 2012", + "id_str": "250075927172759552", + "entities": { + "urls": [ + + ], + "hashtags": [ + { + "text": "freebandnames", + "indices": [ + 20, + 34 + ] + } + ], + "user_mentions": [ + + ] + }, + "in_reply_to_user_id_str": null, + "contributors": null, + "text": "Aggressive Ponytail #freebandnames", + "metadata": { + "iso_language_code": "en", + "result_type": "recent" + }, + "retweet_count": 0, + "in_reply_to_status_id_str": null, + "id": 250075927172759552, + "geo": null, + "retweeted": false, + "in_reply_to_user_id": null, + "place": null, + "user": { + "profile_sidebar_fill_color": "DDEEF6", + "profile_sidebar_border_color": "C0DEED", + "profile_background_tile": false, + "name": "Sean Cummings", + "profile_image_url": "https://a0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", + "created_at": "Mon Apr 26 06:01:55 +0000 2010", + "location": "LA, CA", + "follow_request_sent": null, + "profile_link_color": "0084B4", + "is_translator": false, + "id_str": "137238150", + "entities": { + "url": { + "urls": [ + { + "expanded_url": null, + "url": "", + "indices": [ + 0, + 0 + ] + } + ] + }, + "description": { + "urls": [ + + ] + } + }, + "default_profile": true, + "contributors_enabled": false, + "favourites_count": 0, + "url": null, + "profile_image_url_https": "https://si0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", + "utc_offset": -28800, + "id": 137238150, + "profile_use_background_image": true, + "listed_count": 2, + "profile_text_color": "333333", + "lang": "en", + "followers_count": 70, + "protected": false, + "notifications": null, + "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png", + "profile_background_color": "C0DEED", + "verified": false, + "geo_enabled": true, + "time_zone": "Pacific Time (US & Canada)", + "description": "Born 330 Live 310", + "default_profile_image": false, + "profile_background_image_url": "https://a0.twimg.com/images/themes/theme1/bg.png", + "statuses_count": 579, + "friends_count": 110, + "following": null, + "show_all_inline_media": false, + "screen_name": "sean_cummings" + }, + "in_reply_to_screen_name": null, + "source": "Twitter for Mac", + "in_reply_to_status_id": null + }, + { + "coordinates": null, + "favorited": false, + "truncated": false, + "created_at": "Fri Sep 21 23:40:54 +0000 2012", + "id_str": "249292149810667520", + "entities": { + "urls": [ + + ], + "hashtags": [ + { + "text": "FreeBandNames", + "indices": [ + 20, + 34 + ] + } + ], + "user_mentions": [ + + ] + }, + "in_reply_to_user_id_str": null, + "contributors": null, + "text": "Thee Namaste Nerdz. #FreeBandNames", + "metadata": { + "iso_language_code": "pl", + "result_type": "recent" + }, + "retweet_count": 0, + "in_reply_to_status_id_str": null, + "id": 249292149810667520, + "geo": null, + "retweeted": false, + "in_reply_to_user_id": null, + "place": null, + "user": { + "profile_sidebar_fill_color": "DDFFCC", + "profile_sidebar_border_color": "BDDCAD", + "profile_background_tile": true, + "name": "Chaz Martenstein", + "profile_image_url": "https://a0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", + "created_at": "Tue Apr 07 19:05:07 +0000 2009", + "location": "Durham, NC", + "follow_request_sent": null, + "profile_link_color": "0084B4", + "is_translator": false, + "id_str": "29516238", + "entities": { + "url": { + "urls": [ + { + "expanded_url": null, + "url": "https://bullcityrecords.com/wnng/", + "indices": [ + 0, + 32 + ] + } + ] + }, + "description": { + "urls": [ + + ] + } + }, + "default_profile": false, + "contributors_enabled": false, + "favourites_count": 8, + "url": "https://bullcityrecords.com/wnng/", + "profile_image_url_https": "https://si0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", + "utc_offset": -18000, + "id": 29516238, + "profile_use_background_image": true, + "listed_count": 118, + "profile_text_color": "333333", + "lang": "en", + "followers_count": 2052, + "protected": false, + "notifications": null, + "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/9423277/background_tile.bmp", + "profile_background_color": "9AE4E8", + "verified": false, + "geo_enabled": false, + "time_zone": "Eastern Time (US & Canada)", + "description": "You will come to Durham, North Carolina. I will sell you some records then, here in Durham, North Carolina. Fun will happen.", + "default_profile_image": false, + "profile_background_image_url": "https://a0.twimg.com/profile_background_images/9423277/background_tile.bmp", + "statuses_count": 7579, + "friends_count": 348, + "following": null, + "show_all_inline_media": true, + "screen_name": "bullcityrecords" + }, + "in_reply_to_screen_name": null, + "source": "web", + "in_reply_to_status_id": null + }, + { + "coordinates": null, + "favorited": false, + "truncated": false, + "created_at": "Fri Sep 21 23:30:20 +0000 2012", + "id_str": "249289491129438208", + "entities": { + "urls": [ + + ], + "hashtags": [ + { + "text": "freebandnames", + "indices": [ + 29, + 43 + ] + } + ], + "user_mentions": [ + + ] + }, + "in_reply_to_user_id_str": null, + "contributors": null, + "text": "Mexican Heaven, Mexican Hell #freebandnames", + "metadata": { + "iso_language_code": "en", + "result_type": "recent" + }, + "retweet_count": 0, + "in_reply_to_status_id_str": null, + "id": 249289491129438208, + "geo": null, + "retweeted": false, + "in_reply_to_user_id": null, + "place": null, + "user": { + "profile_sidebar_fill_color": "99CC33", + "profile_sidebar_border_color": "829D5E", + "profile_background_tile": false, + "name": "Thomas John Wakeman", + "profile_image_url": "https://a0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", + "created_at": "Tue Sep 01 21:21:35 +0000 2009", + "location": "Kingston New York", + "follow_request_sent": null, + "profile_link_color": "D02B55", + "is_translator": false, + "id_str": "70789458", + "entities": { + "url": { + "urls": [ + { + "expanded_url": null, + "url": "", + "indices": [ + 0, + 0 + ] + } + ] + }, + "description": { + "urls": [ + + ] + } + }, + "default_profile": false, + "contributors_enabled": false, + "favourites_count": 19, + "url": null, + "profile_image_url_https": "https://si0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", + "utc_offset": -18000, + "id": 70789458, + "profile_use_background_image": true, + "listed_count": 1, + "profile_text_color": "3E4415", + "lang": "en", + "followers_count": 63, + "protected": false, + "notifications": null, + "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme5/bg.gif", + "profile_background_color": "352726", + "verified": false, + "geo_enabled": false, + "time_zone": "Eastern Time (US & Canada)", + "description": "Science Fiction Writer, sort of. Likes Superheroes, Mole People, Alt. Timelines.", + "default_profile_image": false, + "profile_background_image_url": "https://a0.twimg.com/images/themes/theme5/bg.gif", + "statuses_count": 1048, + "friends_count": 63, + "following": null, + "show_all_inline_media": false, + "screen_name": "MonkiesFist" + }, + "in_reply_to_screen_name": null, + "source": "web", + "in_reply_to_status_id": null + }, + { + "coordinates": null, + "favorited": false, + "truncated": false, + "created_at": "Fri Sep 21 22:51:18 +0000 2012", + "id_str": "249279667666817024", + "entities": { + "urls": [ + + ], + "hashtags": [ + { + "text": "freebandnames", + "indices": [ + 20, + 34 + ] + } + ], + "user_mentions": [ + + ] + }, + "in_reply_to_user_id_str": null, + "contributors": null, + "text": "The Foolish Mortals #freebandnames", + "metadata": { + "iso_language_code": "en", + "result_type": "recent" + }, + "retweet_count": 0, + "in_reply_to_status_id_str": null, + "id": 249279667666817024, + "geo": null, + "retweeted": false, + "in_reply_to_user_id": null, + "place": null, + "user": { + "profile_sidebar_fill_color": "BFAC83", + "profile_sidebar_border_color": "615A44", + "profile_background_tile": true, + "name": "Marty Elmer", + "profile_image_url": "https://a0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", + "created_at": "Mon May 04 00:05:00 +0000 2009", + "location": "Wisconsin, USA", + "follow_request_sent": null, + "profile_link_color": "3B2A26", + "is_translator": false, + "id_str": "37539828", + "entities": { + "url": { + "urls": [ + { + "expanded_url": null, + "url": "https://www.omnitarian.me", + "indices": [ + 0, + 24 + ] + } + ] + }, + "description": { + "urls": [ + + ] + } + }, + "default_profile": false, + "contributors_enabled": false, + "favourites_count": 647, + "url": "https://www.omnitarian.me", + "profile_image_url_https": "https://si0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", + "utc_offset": -21600, + "id": 37539828, + "profile_use_background_image": true, + "listed_count": 52, + "profile_text_color": "000000", + "lang": "en", + "followers_count": 608, + "protected": false, + "notifications": null, + "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/106455659/rect6056-9.png", + "profile_background_color": "EEE3C4", + "verified": false, + "geo_enabled": false, + "time_zone": "Central Time (US & Canada)", + "description": "Cartoonist, Illustrator, and T-Shirt connoisseur", + "default_profile_image": false, + "profile_background_image_url": "https://a0.twimg.com/profile_background_images/106455659/rect6056-9.png", + "statuses_count": 3575, + "friends_count": 249, + "following": null, + "show_all_inline_media": true, + "screen_name": "Omnitarian" + }, + "in_reply_to_screen_name": null, + "source": "Twitter for iPhone", + "in_reply_to_status_id": null + } + ], + "search_metadata": { + "max_id": 250126199840518145, + "since_id": 24012619984051000, + "refresh_url": "?since_id=250126199840518145&q=%23freebandnames&result_type=mixed&include_entities=1", + "next_results": "?max_id=249279667666817023&q=%23freebandnames&count=4&include_entities=1&result_type=mixed", + "count": 4, + "completed_in": 0.035, + "since_id_str": "24012619984051000", + "query": "%23freebandnames", + "max_id_str": "250126199840518145" + } + }` + +type TwitterStruct struct { + Statuses []Statuses `json:"statuses"` + SearchMetadata SearchMetadata `json:"search_metadata"` +} + +type Hashtags struct { + Text string `json:"text"` + Indices []int `json:"indices"` +} + +type Entities struct { + Urls []interface{} `json:"urls"` + Hashtags []Hashtags `json:"hashtags"` + UserMentions []interface{} `json:"user_mentions"` +} + +type Metadata struct { + IsoLanguageCode string `json:"iso_language_code"` + ResultType string `json:"result_type"` +} + +type Urls struct { + ExpandedURL interface{} `json:"expanded_url"` + URL string `json:"url"` + Indices []int `json:"indices"` +} + +type URL struct { + Urls []Urls `json:"urls"` +} + +type Description struct { + Urls []interface{} `json:"urls"` +} + +type UserEntities struct { + URL URL `json:"url"` + Description Description `json:"description"` +} + +type User struct { + ProfileSidebarFillColor string `json:"profile_sidebar_fill_color"` + ProfileSidebarBorderColor string `json:"profile_sidebar_border_color"` + ProfileBackgroundTile bool `json:"profile_background_tile"` + Name string `json:"name"` + ProfileImageURL string `json:"profile_image_url"` + CreatedAt string `json:"created_at"` + Location string `json:"location"` + FollowRequestSent interface{} `json:"follow_request_sent"` + ProfileLinkColor string `json:"profile_link_color"` + IsTranslator bool `json:"is_translator"` + IDStr string `json:"id_str"` + Entities UserEntities `json:"entities"` + DefaultProfile bool `json:"default_profile"` + ContributorsEnabled bool `json:"contributors_enabled"` + FavouritesCount int `json:"favourites_count"` + URL interface{} `json:"url"` + ProfileImageURLHTTPS string `json:"profile_image_url_https"` + UtcOffset int `json:"utc_offset"` + ID int `json:"id"` + ProfileUseBackgroundImage bool `json:"profile_use_background_image"` + ListedCount int `json:"listed_count"` + ProfileTextColor string `json:"profile_text_color"` + Lang string `json:"lang"` + FollowersCount int `json:"followers_count"` + Protected bool `json:"protected"` + Notifications interface{} `json:"notifications"` + ProfileBackgroundImageURLHTTPS string `json:"profile_background_image_url_https"` + ProfileBackgroundColor string `json:"profile_background_color"` + Verified bool `json:"verified"` + GeoEnabled bool `json:"geo_enabled"` + TimeZone string `json:"time_zone"` + Description string `json:"description"` + DefaultProfileImage bool `json:"default_profile_image"` + ProfileBackgroundImageURL string `json:"profile_background_image_url"` + StatusesCount int `json:"statuses_count"` + FriendsCount int `json:"friends_count"` + Following interface{} `json:"following"` + ShowAllInlineMedia bool `json:"show_all_inline_media"` + ScreenName string `json:"screen_name"` +} + +type Statuses struct { + Coordinates interface{} `json:"coordinates"` + Favorited bool `json:"favorited"` + Truncated bool `json:"truncated"` + CreatedAt string `json:"created_at"` + IDStr string `json:"id_str"` + Entities Entities `json:"entities"` + InReplyToUserIDStr interface{} `json:"in_reply_to_user_id_str"` + Contributors interface{} `json:"contributors"` + Text string `json:"text"` + Metadata Metadata `json:"metadata"` + RetweetCount int `json:"retweet_count"` + InReplyToStatusIDStr interface{} `json:"in_reply_to_status_id_str"` + ID int64 `json:"id"` + Geo interface{} `json:"geo"` + Retweeted bool `json:"retweeted"` + InReplyToUserID interface{} `json:"in_reply_to_user_id"` + Place interface{} `json:"place"` + User User `json:"user"` + InReplyToScreenName interface{} `json:"in_reply_to_screen_name"` + Source string `json:"source"` + InReplyToStatusID interface{} `json:"in_reply_to_status_id"` +} + +type SearchMetadata struct { + MaxID int64 `json:"max_id"` + SinceID int64 `json:"since_id"` + RefreshURL string `json:"refresh_url"` + NextResults string `json:"next_results"` + Count int `json:"count"` + CompletedIn float64 `json:"completed_in"` + SinceIDStr string `json:"since_id_str"` + Query string `json:"query"` + MaxIDStr string `json:"max_id_str"` +}