{"id":26384543,"url":"https://github.com/douglasmakey/tracking","last_synced_at":"2025-09-22T18:53:56.098Z","repository":{"id":90269509,"uuid":"144172780","full_name":"douglasmakey/tracking","owner":"douglasmakey","description":"A geospatial tracking service with Go and Redis","archived":false,"fork":false,"pushed_at":"2022-03-27T06:51:15.000Z","size":959,"stargazers_count":51,"open_issues_count":0,"forks_count":20,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-06-20T17:33:25.851Z","etag":null,"topics":["article","geospatial","go","golang","microservice","redis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/douglasmakey.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-08-09T15:39:48.000Z","updated_at":"2024-05-19T20:22:22.000Z","dependencies_parsed_at":"2023-09-25T02:09:02.213Z","dependency_job_id":null,"html_url":"https://github.com/douglasmakey/tracking","commit_stats":{"total_commits":16,"total_committers":2,"mean_commits":8.0,"dds":0.125,"last_synced_commit":"fbdad737e3f36646969929d44c4f8a66be29cf3a"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douglasmakey%2Ftracking","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douglasmakey%2Ftracking/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douglasmakey%2Ftracking/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/douglasmakey%2Ftracking/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/douglasmakey","download_url":"https://codeload.github.com/douglasmakey/tracking/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243991736,"owners_count":20380045,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["article","geospatial","go","golang","microservice","redis"],"created_at":"2025-03-17T07:29:35.366Z","updated_at":"2025-09-22T18:53:56.018Z","avatar_url":"https://github.com/douglasmakey.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tracking Service with Go and Redis.\n\n[README V2](https://github.com/douglasmakey/tracking/blob/master/README_V2.md)\n\nImagine that we work at a startup like Uber and we need to create a new service that saves drivers locations every given time and processes it. This way, when someone requests a driver we can find out which drivers are closer to our picking point.\n\nThis is the core of our service. Save the locations and search nearby drivers. For this service we are using Go and Redis.\n\n### Redis\nRedis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. [Redis](http://redis.io)\n\nRedis has multiple functions but for the purpose of this service we are going to focus on its geospatial functions.\n\nFirst we need to install Redis, I recommend using Docker running a container with Redis. By simply following this command, we will have a container running Redis in our machine.\n\n```bash\ndocker run -d -p 6379:6379 redis\n```\n\n\n# Let's start coding\n\nWe are going to write a basic implementation for this service since I want to write other articles on how to improve this service. I will use this code as a base on my next articles.\n\nFor this service we need to use the package \"github.com/go-redis/redis\" that provides a Redis client for Golang.\n\nCreate a new project(folder) in your workdir. In my case I will call it 'tracking'. First we need to install the package.\n\n```bash\ngo get -u github.com/go-redis/redis\n```\n\nThen we create the file 'storages/redis.go' that contains the implementation that will help us  getting a Redis client and some functions to work with geospatial.\n\nWe now create a struct that contains a pointer to the redis client. This pointer will have the functions that help us with this service, we also create a constant with the key name for our set in redis.\n\n```go\ntype RedisClient struct { *redis.Client }\nconst key = \"drivers\"\n\n```\n\nFor the function to get the Redis client, we are going to use the singleton pattern with the help of the sync package and its Once.Do functionality.\n\nIn software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system. If you want to read more about [Singleton Pattern](https://en.wikipedia.org/wiki/Singleton_pattern).\n\nBut how works Once.Do, the struct `sync.Once` has an atomic counter and it uses `atomic.StoreUint32` to set a value to 1, when the function has been called, and then `atomic.LoadUint32` to see if it needs to be called again. For this basic implementation GetRedisClient will be called from two endpoints but we only want to get one instance.\n\n```go\nvar once sync.Once\nvar redisClient *RedisClient\n\nfunc GetRedisClient() *RedisClient {\n\tonce.Do(func() {\n\t\tclient := redis.NewClient(\u0026redis.Options{\n\t\t\tAddr:     \"localhost:6379\",\n\t\t\tPassword: \"\", // no password set\n\t\t\tDB:       0,  // use default DB\n\t\t})\n\n\t\tredisClient = \u0026RedisClient{client}\n        _, err := redisClient.Ping().Result()\n        if err != nil {\n            log.Fatalf(\"Could not connect to redis %v\", err)\n        }\n    })\n\n\treturn redisClient\n}\n\n```\n\nThen we create three functions for the RedisClient.\n\nAddDriverLocation: Add the specified geospatial item (latitude, longitude, name \"in this case name is the driver id\") to the specified key, do you remember the key that we defined at the beginning for our Set in Redis ? This is it.\n\n```go\nfunc (c *RedisClient) AddDriverLocation(lng, lat float64, id string) {\n\tc.GeoAdd(\n\t\tkey,\n\t\t\u0026redis.GeoLocation{Longitude: lng, Latitude: lat, Name: id},\n\t)\n}\n\n```\n\nRemoveDriverLocation: The client redis does not have the function GeoDel because GEODEL command does not exist, so we can use ZREM in order to remove elements. The Geo index structure is just a sorted set.\n\n```go\nfunc (c *RedisClient) RemoveDriverLocation(id string) {\n\tc.ZRem(key, id)\n}\n\n```\n\nSearchDrivers: the function GeoRadius implements the command GEORADIUS that returns the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). If you want to learn more about this go [GEORADIUS](https://redis.io/commands/georadius)\n\n```go\nfunc (c *RedisClient) SearchDrivers(limit int, lat, lng, r float64) []redis.GeoLocation {\n\t/*\n\tWITHDIST: Also return the distance of the returned items from the\n\tspecified center. The distance is returned in the same unit as the unit\n\tspecified as the radius argument of the command.\n\tWITHCOORD: Also return the longitude,latitude coordinates of the matching items.\n\tWITHHASH: Also return the raw geohash-encoded sorted set score of the item,\n\tin the form of a 52 bit unsigned integer. This is only useful for low level\n\thacks or debugging and is otherwise of little interest for the general user.\n\t */\n\t \n\tres, _ := c.GeoRadius(key, lng, lat, \u0026redis.GeoRadiusQuery{\n\t\tRadius:      r,\n\t\tUnit:        \"km\",\n\t\tWithGeoHash: true,\n\t\tWithCoord:   true,\n\t\tWithDist:    true,\n\t\tCount:       limit,\n\t\tSort:        \"ASC\",\n\t}).Result()\n\n\treturn res\n}\n```\n\nNext, create a main.go\n\n```golang\npackage main\n\nimport (\n\t\"net/http\"\n\t\"fmt\"\n\t\"log\"\n)\n\nfunc main() {\n\t// We create a simple httpserver\n\tserver := http.Server{\n\t\tAddr:    fmt.Sprint(\":8000\"),\n\t\tHandler: NewHandler(),\n\t}\n\n\t// Run server\n\tlog.Printf(\"Starting HTTP Server. Listening at %q\", server.Addr)\n\tif err := server.ListenAndServe(); err != nil {\n\t\tlog.Printf(\"%v\", err)\n\t} else {\n\t\tlog.Println(\"Server closed ! \")\n\t}\n\n}\n```\nWe create a simple server using http.Server.\n\nThen we create file 'handler/handler.go' that contains the endpoints for our application.\n\n```golang\nfunc NewHandler() *http.ServeMux {\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"tracking\", tracking)\n\tmux.HandleFunc(\"search\", search)\n\treturn mux\n}\n\n```\n\nWe use http.ServeMux to handle our endpoints, we create two endpoints for our service.\n\nThe first endpoint 'tracking' let's us save the last location sent from a driver, in this case we only want to save the last location. We could modify this endpoint so that previous locations are saved in another database.\n\n```go\nfunc tracking(w http.ResponseWriter, r *http.Request) {\n\t// crate an anonymous struct for driver data.\n\tvar driver = struct {\n\t\tID string `json:\"id\"`\n\t\tLat float64 `json:\"lat\"`\n\t\tLng float64 `json:\"lng\"`\n\t}{}\n\n\trClient := storages.GetRedisClient()\n\n\tif err := json.NewDecoder(r.Body).Decode(\u0026driver); err != nil {\n\t\tlog.Printf(\"could not decode request: %v\", err)\n\t\thttp.Error(w, \"could not decode request\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Add new location\n\t// You can save locations in another db\n\trClient.AddDriverLocation(driver.Lng, driver.Lat, driver.ID)\n\n\tw.WriteHeader(http.StatusOK)\n\treturn\n}\n```\n\nThe second endpoint is 'search' with this endpoint we can find all drivers near a given point,\n\n```go\n// search receives lat and lng of the picking point and searches drivers about this point.\nfunc search(w http.ResponseWriter, r *http.Request) {\n\trClient := storages.GetRedisClient()\n\n\tbody := struct {\n\t\tLat float64 `json:\"lat\"`\n\t\tLng float64 `json:\"lng\"`\n\t\tLimit int `json:\"limit\"`\n\t}{}\n\n\tif err := json.NewDecoder(r.Body).Decode(\u0026body); err != nil {\n\t\tlog.Printf(\"could not decode request: %v\", err)\n\t\thttp.Error(w, \"could not decode request\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdrivers := rClient.SearchDrivers(body.Limit, body.Lat, body.Lng, 15)\n\tdata, err := json.Marshal(drivers)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(http.StatusOK)\n\tw.Write(data)\n\treturn\n}\n\n```\n\n# Let's test the service\n\nFirst, run the server.\n```bash\ngo run main.go\n```\n\nNext, we need to add four drivers locations.\n\n![Map example screenshot](https://github.com/douglasmakey/tracking/blob/master/example.png?raw=true)\n\nWe add four drivers as above in the map, the lines green show distance between picking point and drivers.\n\n\n```bash\ncurl -i --header \"Content-Type: application/json\" --data '{\"id\": \"1\", \"lat\": -33.44091, \"lng\": -70.6301}' http://localhost:8000/tracking\n\ncurl -i --header \"Content-Type: application/json\" --data '{\"id\": \"2\", \"lat\": -33.44005, \"lng\": -70.63279}' http://localhost:8000/tracking\n\ncurl -i --header \"Content-Type: application/json\" --data '{\"id\": \"3\", \"lat\": -33.44338, \"lng\": -70.63335}' http://localhost:8000/tracking\n\ncurl -i --header \"Content-Type: application/json\" --data '{\"id\": \"4\", \"lat\": -33.44186, \"lng\": -70.62653}' http://localhost:8000/tracking\n```\n\nSince we now have the locations of the drivers, we can do a spacial search.\n\n\nwe will look for 4 nearby drivers\n\n```bash\ncurl -i --header \"Content-Type: application/json\" --data '{\"lat\": -33.44262, \"lng\": -70.63054, \"limit\": 5}' http://localhost:8000/search\n```\n\nAs you will see the result matches with the map, see the lines greens in the map.\n\n```bash\nHTTP/1.1 200 OK\nContent-Type: application/json\nDate: Wed, 08 Aug 2018 05:07:57 GMT\nContent-Length: 456\n\n[\n    {\n        \"Name\": \"1\",\n        \"Longitude\": -70.63009768724442,\n        \"Latitude\": -33.44090957099124,\n        \"Dist\": 0.1946,\n        \"GeoHash\": 861185092131738\n    },\n    {\n        \"Name\": \"3\",\n        \"Longitude\": -70.63334852457047,\n        \"Latitude\": -33.44338092412159,\n        \"Dist\": 0.2741,\n        \"GeoHash\": 861185074815667\n    },\n    {\n        \"Name\": \"2\",\n        \"Longitude\": -70.63279062509537,\n        \"Latitude\": -33.44005030051822,\n        \"Dist\": 0.354,\n        \"GeoHash\": 861185086448695\n    },\n    {\n        \"Name\": \"4\",\n        \"Longitude\": -70.62653034925461,\n        \"Latitude\": -33.44186009142599,\n        \"Dist\": 0.3816,\n        \"GeoHash\": 861185081504625\n    }\n]\n```\n\n\nLook up for the nearest driver\n\n```bash\ncurl -i --header \"Content-Type: application/json\" --data '{\"lat\": -33.44262, \"lng\": -70.63054, \"limit\": 1}' http://localhost:8000/search\n```\n\nResult\n\n```bash\nHTTP/1.1 200 OK\nContent-Type: application/json\nDate: Wed, 08 Aug 2018 05:12:24 GMT\nContent-Length: 115\n\n[{\"Name\":\"1\",\"Longitude\":-70.63009768724442,\"Latitude\":-33.44090957099124,\"Dist\":0.1946,\"GeoHash\":861185092131738}]\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouglasmakey%2Ftracking","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdouglasmakey%2Ftracking","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdouglasmakey%2Ftracking/lists"}