How does Go's encoding/xml decide which struct field an XML element or attribute fills?
answer
- reflection can only set exported fields
- tag name first, field name otherwise
- an attribute is not a child element
- XMLName is the one that can complain
- no match, no value, no error
basics
~20 sencoding/xml looks only at exported fields. An element fills the field whose xml tag name, or field name if untagged, matches it; an attribute needs a tag ending in ,attr. Anything unmatched is dropped silently.
solid answer
~50 s`xml.Unmarshal` reflects over the destination struct and matches by name. Only **exported** fields are eligible — an unexported field is invisible to the package and stays at its zero value. For each field it takes the name from the `xml:"..."` tag, or the Go field name when there is no tag, and compares it with the element's name. A field tagged `,attr` is matched against the element's *attributes* instead of its children, so `xml:"id"` will never pick up `id="7"`. A field named `XMLName` of type `xml.Name` is special: it records the name of the element the struct was decoded from, and if that field carries a tag the element must have that name or `Unmarshal` returns an error. Repeated elements need a slice field. Input that matches no field is discarded with no error at all.
code
go · 7 linestype Item struct {
XMLName xml.Name `xml:"item"`
ID string `xml:"id,attr"` // <item id="7">
Title string `xml:"title"` // <title>Bolt</title>
SKU string `xml:"sku"` // stays empty if sku is an attribute
price string `xml:"price"` // unexported: never filled
}go deeper
Be ready to state the three rules that decide everything: exported fields only, the tag name (or field name) must match the element name, and attributes need ,attr. Know that a mismatch is silent.
Explain the mechanism, not just the rules: reflection over the struct type, tags as inert metadata the package chooses to read, XMLName as the one construct that turns a mismatch into an error, and slices for repetition.
Show the operational consequence: a nil error from xml.Unmarshal proves the bytes parsed, not that the data landed. Say how you assert a decode actually populated what you expected before shipping a converter.
Own the call on how much hand-maintained tag surface a team should carry for a third-party feed at all, and what evidence — a replayed vendor sample under test — is required before a converter is allowed to depend on it.
## The question encoding/xml asks `xml.Unmarshal(data []byte, v any) error` walks the document and, for every element and attribute it meets, asks the destination struct type a single question: *which of your fields wants this?* The answer comes entirely from the struct's shape and its field tags, read by reflection. Nothing else — no registration, no schema, no generated code — takes part. ## Rule 1: exported fields only A field whose name starts with a lower-case letter cannot be set through reflection, so `encoding/xml` skips it. It is not an error; the field simply keeps its zero value. This is the single most common surprise for someone porting a hand-rolled parser, where the parser could of course assign to any field it liked. ```go type Item struct { Title string `xml:"title"` // filled price string `xml:"price"` // never filled: unexported } ``` ## Rule 2: the name comes from the tag, then the field name Each field's XML name is the leading part of its `xml:"..."` struct tag. With no tag, the Go field name is used as-is. So `Title string` matches `<Title>`, while ``Title string `xml:"title"` `` matches `<title>`. Because XML documents are usually written in a naming style that does not match Go's exported-identifier style, most real wire structs tag every field. A struct tag is an ordinary string literal held in the type's metadata — inert as far as the compiler is concerned. It only means something because `encoding/xml` chooses to read it. ## Rule 3: attributes are a different namespace from elements XML has two places a value can live: a child element and an attribute of the current element. `encoding/xml` keeps them strictly apart. A field is matched against attributes only if its tag has the `,attr` option: ```go type Item struct { ID string `xml:"id,attr"` // <item id="7"> Title string `xml:"title"` // <title>...</title> } ``` Getting this backwards produces an empty field and no error. JSON has no attribute concept at all, so the distinction is one of the first things to internalise when moving to XML. ## Rule 4: XMLName If the struct has a field named `XMLName` of type `xml.Name`, `Unmarshal` records into it the name of the element the struct was decoded from. If that field also has a tag, the tag becomes a constraint: the element must have that name or `Unmarshal` returns an error. That makes `XMLName` a cheap tripwire — the one place where the package will actually complain that you pointed it at the wrong document, rather than handing you an empty struct. ```go type Feed struct { XMLName xml.Name `xml:"rss"` // decoding <feed> here is an error } ``` On the encoding side, `xml.Marshal` uses the same field to choose the element name it writes. ## Rule 5: shapes - A **struct** field means "descend into this element". - A **slice** field accumulates every occurrence of a repeated element. A non-slice field decoded from three matching elements ends up holding the last one. - A **pointer** field is allocated when a matching element appears, so it distinguishes an absent element from an empty one. - An **embedded (anonymous) struct** has its fields treated as if they were declared in the outer struct. ## Rule 6: no match is not an error Elements and attributes that match no field are skipped in silence, and fields that no input matched keep their zero values. `encoding/xml` returns an error for malformed XML, for a type it cannot fill, and for an `XMLName` tag mismatch — but never for "the document said something you did not ask about" or "you asked for something the document did not say". The practical consequence: a decode that returns `nil` proves the bytes were well-formed XML, not that you got the data you wanted. When a field comes back empty, the bug is in the matching rules above, not in the document.
- What happens if the same element name appears three times but the struct field is a plain string?Each occurrence overwrites the previous one, so you are left with the text of the last one and no indication that the others existed. Repeated elements belong in a slice field — `Tags []string \u0060xml:"tag"\u0060` collects all three in document order.
- Does an untagged XMLName field do anything useful?Yes. Without a tag it imposes no constraint, but `Unmarshal` still records the name of the element it decoded into it. That is handy when the same struct is used for several element names, or as a probe while you are working out what a vendor document actually contains.
- Why does encoding/xml need exported fields when a hand-written parser does not?It fills the struct through reflection, and the reflect package refuses to set unexported fields — that is the language's package-boundary rule, not a decision of the xml package. Any reflection-driven codec in Go has the same limitation, which is why wire structs are written with exported fields and tags.
Think of the struct as a form with pre-printed field labels. encoding/xml files each value under the label that matches; anything with no matching label goes in the bin, and no one tells you.
saying these in an interview costs you the question
- Thinks unexported fields are filled like a hand-written parser fills them
- Expects a missing element to make xml.Unmarshal return an error
- Expects a plain tag to pick up an attribute of the same name
- Believes unknown elements in the document cause a decode failure
- Thinks a single string field collects every occurrence of a repeated element