This is a Go package for parsing GTFS static and realtime feeds.
The static parser is pretty straightforward and simply maps fields in the GTFS static CSV files to associated Go types.
For realtime parsing this package offers two options.
Option 1 is to use the parser and types generated by the protobuf compiler which are available in the proto subpackage.
This saves you from having to compile the protobufs yourself.
Option 2 is to use the package's custom parser, which takes the protobuf output from option 1, does a bunch of post-processing, and then outputs the result in standard (non-proto) Go types. This post-processing does useful things like:
-
Converts some data fields to more idiomatic Go types - for example, times are converted from
int64Unix timestamps totime.Timevalues. -
Makes the representation of trips and vehicles more explicit. Each trip has a (nullable) pointer to the associated vehicle in the feed, if it exists, and vice-versa.
-
Implements business logic to map data in various GTFS realtime extensions to regular GTFS realtime fields.
Backwards compatibility warning:
this package is under active development and backwards incompatible changes are frequently made.
We're eventually planning to release a v1.0.0 version, and after that all changes
will be backwards compatible and consistent with semantic versioning.
Go version support:
this package supports the current Go release and the two releases before it.
When a new Go release comes out, the minimum version in go.mod moves up to match.
The minimum is currently Go 1.25.
Parse the GTFS static feed for the New York City Subway:
resp, _ := http.Get("http://web.mta.info/developers/data/nyct/subway/google_transit.zip")
b, _ := io.ReadAll(resp.Body)
staticData, _ := gtfs.ParseStatic(b, gtfs.ParseStaticOptions{})
fmt.Printf("The New York City subway has %d routes and %d stations\n", len(staticData.Routes), len(staticData.Stops))Stop times take most of the memory of a parsed feed. If you don't need arrival and departure times or shapes, ask the parser to leave them out:
staticData, _ := gtfs.ParseStatic(b, gtfs.ParseStaticOptions{
SkipShapes: true,
// Keep only the stops each trip visits, in trip.Stops.
// Use gtfs.StopTimesNone to skip stop_times.txt entirely.
StopTimes: gtfs.StopTimesStopsOnly,
})Parse the GTFS realtime feed for the San Francisco Bay Area BART:
resp, _ := http.Get("http://api.bart.gov/gtfsrt/tripupdate.aspx")
b, _ := io.ReadAll(resp.Body)
realtimeData, _ := gtfs.ParseRealtime(b, >fs.ParseRealtimeOptions{})
fmt.Printf("The SF BART currently has %d trains running or scheduled\n", len(realtimeData.Trips))The table lists every file in the GTFS Schedule reference as revised on April 27, 2026, and whether the parser reads it. Please report gaps in this repository's issues.
| File Name | Supported | Required by Spec | Notes |
|---|---|---|---|
| agency.txt | ✅ | Required | |
| stops.txt | ✅ | Conditionally Required | Optional only when locations.geojson is present |
| routes.txt | ✅ | Required | |
| trips.txt | ✅ | Required | |
| stop_times.txt | ✅ | Required | |
| calendar.txt | ✅ | Conditionally Required | Surfaced as a Service. GTFS requires it unless calendar_dates.txt defines every date of service; the library treats it as optional |
| calendar_dates.txt | ✅ | Conditionally Required | Surfaced as part of a Service. Optional to the library |
| fare_attributes.txt | ✅ | Optional | GTFS-Fares V1 (#30) |
| fare_rules.txt | ✅ | Optional | GTFS-Fares V1 (#30) |
| timeframes.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| rider_categories.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| fare_media.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| fare_products.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| fare_leg_rules.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| fare_leg_join_rules.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| fare_transfer_rules.txt | ✅ | Optional | GTFS-Fares V2 (#31) |
| areas.txt | ✅ | Optional | |
| stop_areas.txt | ✅ | Optional | |
| networks.txt | ✅ | Conditionally Forbidden | |
| route_networks.txt | ✅ | Conditionally Forbidden | |
| location_groups.txt | ✅ | Optional | |
| shapes.txt | ✅ | Optional | |
| frequencies.txt | ✅ | Optional | |
| transfers.txt | ✅ | Optional | #17, #18, #19; see upgrading |
| pathways.txt | ✅ | Optional | |
| levels.txt | ✅ | Conditionally Required | |
| location_group_stops.txt | ✅ | Optional | |
| locations.geojson | ✅ | Optional | Polygon and MultiPolygon only |
| booking_rules.txt | ✅ | Optional | |
| translations.txt | ✅ | Optional | |
| feed_info.txt | ✅ | Conditionally Required | |
| attributions.txt | ✅ | Optional |
Feeds that use GTFS-Flex now parse into
Static.Locations, Static.LocationGroups and Static.BookingRules, and their
stop_times.txt rows appear on trips alongside ordinary ones. Code written against
earlier versions should note:
- A row's GTFS-Flex columns are on
ScheduledStopTime.Flex, a*FlexStopTimethat is nil when the row sets none of them. Accessors such asLocation()andPickupBookingRule()return nil in that case, so callers need not checkFlexfirst. ScheduledStopTime.Stopmay be nil. A flex row references aFlex.Locationor aFlex.LocationGroupinstead; exactly one ofStop,LocationandLocationGroupis set.- A windowed row (one with
Flex.StartPickupDropOffWindow/EndPickupDropOffWindow) has zeroArrivalTimeandDepartureTimeandExactTimes == false. Interpolation leaves these rows alone. - Code that expects fixed-stop, fixed-time service should skip flex rows with
IsFlex(), or only windowed rows withIsWindowed(). - A
stop_times.txtrow whosestop_idis not instops.txtis still skipped, but it now produces aStopTimeInvalidReferencewarning inStatic.Warnings. Earlier versions dropped such rows silently. - A row whose
pickup_booking_rule_idordrop_off_booking_rule_iddoes not resolve is kept with a nil booking rule and aStopTimeInvalidReferencewarning. - A
locations.geojsonthat cannot be read as a FeatureCollection is ignored with aLocationsFileInvalidwarning; a single unusable Feature is skipped with aLocationInvalidGeometrywarning. Neither stops the rest of the feed from parsing.
The parser reads location_type as GTFS defines it. Users upgrading from earlier versions
should note:
- A stop with a parent station is no longer read as
StopType_Platform. It is read as itslocation_type, usuallyStopType_Stop. To tell a platform from a standalone stop, checkstop.Parent != nil.StopType_Platformis deprecated and never produced. - A
location_typethat is not empty or0through4adds aStopInvalidLocationTypewarning. The stop is still kept asStopType_Stop, as before. Whitespace around the value is ignored, so1now parses asStopType_Station.
transfer_typevalues4and5(linked trips) now parse asTransferType_InSeatandTransferType_InSeatNotAllowed. Earlier versions read them asTransferType_Recommended.- A row whose
transfer_typeis not empty or0through5is skipped with aTransferInvalidTypewarning. Earlier versions read it asTransferType_Recommended. Whitespace around the value is ignored, so2now parses asTransferType_RequiresTime. - For linked trips,
from_stop_idandto_stop_idare optional, soTransfer.FromandTransfer.Tomay be nil. Check them before use. - A linked-trip row without a known
from_trip_idandto_trip_idis skipped with aTransferMissingTriporTransferUnknownReferencewarning. - A row whose
from_stop_idorto_stop_idis not a stop or station (location_type0 or 1), such as an entrance, is skipped with aTransferStopNotAllowedwarning. For linked trips, only a stop (location_type0) is allowed. - A row that sets both a trip and a route on the same side (
from_orto_) is skipped with aTransferTripNotOnRoutewarning when the trip runs on a different route. - A row whose
from_stop_idequals itsto_stop_idis kept. Earlier versions dropped it. - A row whose
from_route_id,to_route_id,from_trip_idorto_trip_idis not empty and not defined inroutes.txtortrips.txtis skipped with aTransferUnknownReferencewarning. Earlier versions kept the row with a nil route or trip, which made the rule apply to every route or trip at the stop. - A row whose
from_stop_idorto_stop_idis not defined instops.txtis skipped with aTransferUnknownReferencewarning. Earlier versions skipped it with only a log line. - A row of
transfer_typeempty or0to3with an emptyfrom_stop_idorto_stop_idis skipped with aTransferMissingStopwarning. If the file has nofrom_stop_idorto_stop_idcolumn, the first such row adds oneMissingColumnswarning for the file. Earlier versions skipped these rows with only a log line.
The package is designed to be about as fast as possible without resorting to unreadable code.
A profiler for the package is in the performance directory.
The static parser has been performance optimized somewhat significantly. It takes about 3 seconds to parse the GTFS static data for the NYC buses (45 megabytes compressed, 360 megabytes uncompressed). The rough breakdown is:
-
30% of the time unzipping the archives (using the
archive/zipstandard library package). -
40% of the time parsing the CSV files into strings (using the
encoding/csvstandard library package) -
30% of the time in this package performing the conversions from strings into types like
time.Durationand linking related entities.
TBD
The proto subpackage is generated from the .proto files next to it. After editing a
.proto file, install buf (CI uses v1.73.0) and run:
buf generateCI fails if the committed code is out of date.