Not affiliated with Go, Google, W3C, or WHATWG.
1. Introduction
This section is non-normative.💡 This document was inspired by Java language binding for Web IDL and Web IDL Standard JavaScript Bindings. This is just a rough collection of ideas on how to map types from Web IDL Standard to Go. It’s like a miniature reference book. If you have a better way to do things, chances are you’re right! Open an Issue! I’m always looking for more egonomic & better ways to translate Web IDL concepts to Go. ❤️
1.1. Use case: Ergonomic Go ↔ Web API bindings
There currently is a lack of high-quality Go bindings to Web APIs for use when writing Go code that targets GOOS=js
GOARCH=wasm
.
dominikh/go-js-dom provides a lot of bindings to access DOM Standard APIs.
-
It doesn’t let you cast very easily. It’s inconsistent with some types being
struct
and others beinginterface
. The DX and ergonomics could be improved by usinginterface
everywhere. -
It doesn’t provide modern [DOM] features like
append
. It’s a bit outdated. This could be improved with a Web IDL 👉 Go scaffolding generator and more formalized Go ↔ Web IDL rules. -
It doesn’t handle optional primitives at all.
nodeValue
is aDOMString?
in [WEBIDL] but is defined asNodeValue() string
in go-js-dom. Usingnil
-able types would be more idiomatic Go. -
It doesn’t work well with complex function signatures like
addEventListener(type, callback, options)
which has a callback interface and a union type. A standardized well-known way to idiomatically convert [WEBIDL] overloads, union types, and optional parameters to Go would be helpful.
realPy/hogosuru is a Go web framework that offers some Web API bindings too.
-
It doesn’t embrace union types.
addEventListener(type, callback, options)
is represented only asAddEventListener(string, func(Event))
with no options parameter. An ergonomic way to idiomatically represnt overloads and optional parameters in Go would be nice. -
Packages are fragmented. There are so many packages! One for each [DOM] definition is too many. A more cohesive API might group things based on which specification ([DOM], [FETCH], [WEB-SHARE], etc.) that they come from.
-
Everything returns a
(T, error)
. This is just a gripe of how granular I think the error handling is. -
nodeValue
is represented asNodeValue() string
. ThenodeValue
of a node is actuallyDOMString?
though. A standard way to represent nullable [WEBIDL] values in Go (particularily primitives) would help.
1.2. Use case: Path to port Web APIs to Go
URL Pattern Standard, DOM Standard, Fetch Standard, Web Share API, Clipboard API and events, and more are all cool Web APIs that could be ported to Go. Not because being Web IDL-based is better than a custom API, but because it gives developers a very concrete API surface to cover. And as a side effect it makes it very easy to bind to the existing JavaScript bindings of the same Web API.
🌎 JavaScript APIs are also very familiar to most web developers and it’s very cool 😎 when you can use the same concepts and libraries uniformly across domains and languages. It’s just cool.
2. Go binding
-
You should use Go 1.11 modules when developing Go Web IDL bindings. That means
go mod init
and notGOPATH
. -
Try to group things based on which specification they are a part of. This helps segement the vast collection of browser APIs into develop-able chunks.
-
Put everything on one level. Don’t nest things in sub-packages; put it all on the root. Some developers will want to
import . "github.com/octocat/go-fetch"
to makeFetch()
and all associated things top-level. -
The
Window
,WorkerGlobalScope
, etc. interfaces are all ✨special since they are global interfaces. Anything that would be defined as a method onWindow
or other global scope interface should be defined directly at the top level of the Go package instead of as methods on aWindow
struct
/interface
. You should also omit theWindow
and other global interfaces from the types that you define. Think ofWindow
as an invisible package-level interface instead of a concrete type. -
Many Web IDL definitions (particularily in HTML Standard) will make reference to "the global document" or "the current window". There is no "current window" in a typical Go program. You should try to follow the spirit if not the letter of these definitions.
partial interface Window {undefined alert (DOMString message ); };
func Alert( messagestring ) { // ... }
2.1. Names
Remember: to be exported from a package a Go name must start with an uppercase letter.
Use your own best judgement to split the words and PascalCase-ify them to make them conform to the Go convention. Note that initialisms should be all-caps.
For const & static members of interfaces, use the interface name followed by the member name.
For Web IDL constructors use New
followed by the interface name.
innerHTML
| e.InnerHTML() |
---|---|
getElementById
| document.GetElementByID() |
htmlFor
| e.HTMLFor() |
url
| r.URL() |
XMLHttpRequest
| XMLHTTPRequest |
HTMLHtmlElement
| HTMLHTMLElement |
HTMLPreElement
| HTMLPreElement |
Node .TEXT_NODE
| NodeTextNode |
doctype
| document.Doctype() |
id
| e.ID() |
new Document
| NewDocument |
2.2. Go type mapping
Can’t find a Web IDL type defined in this section? Add it yourself or open an issue. ❤️
The following list of types doesn’t require much explaining.
any | any |
---|---|
boolean | bool |
byte | int8 |
octet | byte |
short | int16 |
unsigned short | uint16 |
long | int32 |
unsigned long | uint32 |
long long | int64 |
unsigned long long | uint64 |
unrestricted float | float32 |
unrestricted double | float64 |
bigint | math/big.Int |
DOMString | string |
ByteString | string |
2.2.1. Restricted float and double
Reminder: the unrestricted double
type means that the value can be NaN
, +Infinity
, or -Infinity
. The counterpart is the plain old double
type which cannot be NaN
, +Infinity
, or -Infinity
.
Web IDL float
values in Go are represented as Go float32
values. Web IDL double
values in Go are represented as Go float64
values.
Implementers should panic()
if their input Go float32
or float64
value is NaN
, +Infinity
, or -Infinity
when the Web IDL signature uses a restricted float
or double
type.
partial interface Window {double add (double a ,double b );unrestricted double addUnrestricted (unrestricted double a ,unrestricted double b ); };
func Add( a, bfloat64 ) float64 { if math. IsNaN( a) || math. IsInf( a, 0 ) { panic( "a is not a double" ) } if math. IsNaN( b) || math. IsInf( b, 0 ) { panic( "b is not a double" ) } return a+ b} func AddUnrestricted( a, bfloat64 ) float64 { return a+ b}
2.2.2. undefined
In function return position undefined
should be omitted from the Go function signature.
interface Storage {setter undefined setItem (DOMString name ,DOMString value ); // ... };
type Storageinterface { SetItem( namestring , valuestring ) }
**When used in sequence<undefined>
** or other places where a type is required use struct{}
.
When part of a type union treat it like a nullable ?
type.
interface CustomElementRegistry { (CustomElementConstructor or undefined )get (DOMString name ); // ... };callback CustomElementConstructor =HTMLElement ();
type CustomElementRegistryinterface { // CustomElementConstructor is a nil-able type. Get( namestring ) CustomElementConstructor} type CustomElementConstructor= func () HTMLElement
2.2.3. USVString
The USVString type corresponds to scalar value strings. Depending on the context, these can be treated as sequences of either 16-bit unsigned integer code units or scalar values.
Honestly I’m not entirely sure how to represent USVString
in Go.
If you have a better explaination or some other insight into what DOMString
vs USVString
means in the context of a Go string I’m all ears. 😊
func DoThing( vstring ) { if ! utf8. Valid( v) { panic( "v is not a well-formed UTF-8 string" ) } }
2.2.4. object
object
is similar to any
but it **cannot be null
or a primitive**. In Go-land that means it’s like any
just with a panic()
check to make sure it’s not nil
or a string|int|bool|etc...
type.
func DoThing( vany ) { if v== nil || reflect. ValueOf( v). Kind() < reflect. Array{ panic( "v is not object type" ) } }
2.2.5. symbol
Not yet defined.
2.2.6. Interface types
To support downcasting (Node
👉 HTMLAnchorElement
) easily Web IDL types are represented as Go interface
instead of struct
. Each Web IDL interface
type should have a corresponding Go interface
type. Unless it’s Window
or another global interface, in which case it should be omitted and all its members should go in the package scope.
interface Node :EventTarget {readonly attribute DOMString nodeName ; };interface Window {undefined alert (DOMString message ); };
type Nodeinterface { EventTarget NodeName() string } func Alert( messagestring ) {}
You can’t downcast a struct
*Node
into a struct
*HTMLAudioElement
in Go. You can only cast with interface
-es. This means that if .QuerySelector()
returns a *Element
then there’s no way to try to instanceof HTMLAnchorElement
or instanceof HTMLVideoElement
and then cast it to those types to use .Click()
or .Play()
.
element, err:= document. QuerySelector( "audio" ) // This won't work audio:= element.( * HTMLAudioElement) // So how do you convert element (which is *Element) into an *HTMLAudioElement?
element, err:= document. QuerySelector( "audio" ) // This is how you do it audio:= element.( HTMLAudioElement)
Interface constructors are pulled out into a New___()
function where you fill in the blank with the interface name. For example the Request
interface has a constructor NewRequest()
. For interfaces without a constructor you should omit this New___()
function. Treat interface constructors as though they were their own Window
function and apply all the same union, overload, and optional rules.
[Exposed =(Window ,Worker )]interface Headers {constructor (optional HeadersInit init ); // ... };
func NewHeaders() Headers{} func NewHeadersInit( initany ) Headers{}
2.2.6.1. interface mixin
Mixins are used by Web IDL to define a set of functions implemented by multiple interfaces. The best example of this is the Body
mixin which defines arrayBuffer
, text
, and json
on Request
and Response
objects.
In Go, that just corrolates to an embedded interface!
interface mixin Body {readonly attribute boolean bodyUsed ; [NewObject ]Promise <ArrayBuffer >arrayBuffer (); // ... };Request includes Body ;Response includes Body ;
type Bodyinterface { BodyUsed() bool ArrayBuffer() func () ([] byte , error ) // ... } type Requestinterface { Body} type Responseinterface { Body}
2.2.7. Callback interface types
Callback interfaces are a way to codify the below into Web IDL:
// Both of these work! globalThis. addEventListener( "load" , { handleEvent: () => console. log( "handleEvent" ) }) globalThis. addEventListener( "load" , () => console. log( "function" ))
It’s rarely used.
Treat callback interfaces as though they were just a union type of the function signature and an unnamed interface with that single method defined. Practically, this means that callback interfaces are represented as any
.
You should provide a same-name type TheCallbackInterface func(...)...
type as well as an interface
type with the suffix Object
like type TheCallbackInterfaceObject interface {...}
.
interface Thing {undefined addEventListener (listener EventListener ); };callback interface EventListener {undefined handleEvent (Event event ); };
type EventListenerfunc ( event Event) type EventListenerObjectinterface { HandleEvent( event Event) } func ( e* thingImpl) AddEventListener( listenerany ) { if b, ok:= a.( EventListenerObject); ok{ // Can use like this: b. HandleEvent( event) } else if b, ok:= a.( EventListener); ok{ // Can use like this: b( event) } else { panic( "listener is not an EventListener or EventListenerObject" ) } }
2.2.8. Dictionary types
Web IDL interfaces map quite well to Go struct
. The biggest obstacle here is optional struct
fields. Since Go doesn’t have a Rust Option<T>
or a Java null
-able primitive (Integer
, Double
, String
) objects it means we need to use something else. The most Go-like way of doing things is with nil
-able types. For interfaces this is no sweat: interfaces are always nil
-able. For primitives it means passing by *string
instead of string
.
Go doesn’t let you to take the &
address of literal values. You can use a helper function like this for that:
func ptr[ Tany ]( v T) * T{ return & v} func DoThing( v* string ) {} func main() { // Doesn't work 😢 // DoThing(&"hello ptr magic!") // Works! 😄 DoThing( ptr( "hello ptr magic!" )) }
dictionary ResponseInit {unsigned short status = 200;ByteString statusText = "";HeadersInit headers ; };
type ResponseInitstruct { Status* uint16 StatusText* string Headersany }
Dictionaries that extend other dictionaries should be completely flattened into a single struct
. This is more ergonomic from a consumer’s perspective since all the fields are flattened one level deep instead of being nested.
dictionary AddEventListenerOptions :EventListenerOptions {boolean passive =false ;boolean once =false ; };dictionary EventListenerOptions {boolean capture =false ; };
Here’s an example of what this "flattening" aims to avoid. 🛑 This is the *incorrect* way to represent this Web IDL dictionary.
type AddEventListenerOptionsstruct { EventListenerOptions// 👈 Embedded struct! Passive* bool Once* bool } type EventListenerOptionsstruct { Capture* bool }
// 🤷♀️ Not ideal. AddEventListener( "click" , listener, AddEventListenerOptions{ EventListenerOptions: EventListenerOptions{ Capture: ptr( true ), }, Passive: ptr( true ), Once: ptr( true ), })
Why is this bad? Because it introduces an unnecessary level of nesting when constructing a dictionary which has an inheritance chain like AddEventListenerOptions
. Since most of the use of these dictionaries will be in constructing them, not reading from them (reading admittedly is very ergonomic with Go struct embedding) it’s better to flatten them for the benefit of the user.
// 👍 Better. AddEventListener( "click" , listener, AddEventListenerOptions{ Capture: ptr( true ), Passive: ptr( true ), Once: ptr( true ), })
I’m open to being wrong about nested structs being bad. Give me your experience! Maybe embedded structs are better.
2.2.9. Enumeration types
Todo
2.2.10. Callback function types
Use a type TheType func(...)...
newtype.
callback TheCallback =long (long a ,long b );
type TheCallbackfunc ( aint32 , bint32 ) int32 func useTheCallback( cb TheCallback) { fmt. Printf( "Result: %d" , cb( 1 , 2 )) } func main() { useTheCallback( func ( aint32 , bint32 ) int32 { return a+ b}) }
2.2.11. Nullable types
Nullable types become nil
-able types in Go. All Web IDL interface types should be nil
-able by default since all Go interface
types are nil
-able. Go primitives like string
, int32
, etc. can all be made nil
-able through a pointer. Go structs can be made nil
-able through a pointer as well.
partial interface Window {undefined doThing (DOMString ?message ); };
func DoThing( message* string ) { fmt. Println( message) }
2.2.12. Sequences
For now Web IDL sequences are analagous to Array<T>
types. Each sequence<T>
type can be exactly mapped into a Go []T
slice.
In the future iterables might be accepted.
partial interface Window {undefined doThing (sequence <DOMString >items ); };
func DoThing( items[] string ) { for i, v:= range items{ fmt. Printf( "item %d: %s" , i, v) } }
2.2.13. Records
record<K, V>
maps to map[K]V
in Go.
DOMString
or USVString
.
partial interface Window {undefined doThing (record <DOMString ,long >things ); };
func DoThing( thingsmap [ string ] int32 ) { for k, v:= range things{ fmt. Println( k, v) } }
So far all use-cases for record<K, V>
have been unordered so to Go random map[K]V
ordering is OK.
2.2.14. Promise types
Promises are a bit tricky in Go. Unlike <-chan T
, a Promise<T>
can be await
-ed multiple times and will always spit out the same cached value. What this means is that just <-chan T
isn’t enough to represent a Promise<T>
in Go. It falls apart specifically when you have a static Promise<T>
that you want to await
multiple times like ready
.
Instead, the solution is to use a getter function. The current idiom that seems favored by Go is to *return something with a .Wait()
method* and then call that to "await" the function’s result.
Promise<T> | interface{ Wait() (T, error) } |
---|---|
Promise<undefined> | interface{ Wait() error } |
Infalliable Promise<T> | interface{ Wait() T } |
Infalliable Promise<undefined> | interface{ Wait() } |
partial interface Window {Promise <Response >fetch (); };
func Fetch() interface { Wait() ( Response, error )} { // ... }
An infalliable promise is a promise that never errors. These are extremely rare in Web APIs since most things that are async call system-level operations that can often fail for arbitrary reasons. If you see a Promise<T>
in Web IDL it’s almost always a Promise<T>
which should be a interface { Wait() (T, error) }
in Go.
ready
is an example of the rare case where a Promise<T>
will never reject.
partial interface ServiceWorkerContainer {readonly attribute Promise <ServiceWorkerRegistration >ready ; };
type ServiceWorkerContainerinterface { Ready() interface { Wait() ServiceWorkerRegistration} }
In all cases the returned object should support being called concurrently. sync.*
friends handle this for you.
The work that the operation is doing should be done in a go func(){}()
Go-routine in an async fashion. The Promise<T>
exposes the result to the caller.
2.2.15. Union
Web IDL union types are represented as any
in Go. Then at runtime they are if b, ok := a.(T); ok {}
checked to see if they fit the union terminal types. There’s no recursion for union types that contain union types; all of said types must be individually checked for at runtime using type conversion checks.
You should provide a stub type UnionType = any
type alias (not a newtype) with a documentation comment outlining which values are accepted in the union.
Why not type UnionType any
as a newtype? Because then it becomes an opaque type that is impossible to downcast to any of the terminal types.
typedef (DOMString or long )TheUnion ;partial interface Window {undefined doThing (TheUnion u ); };
type TheUnionany func DoThing( u TheUnion) { if uString, ok:= u.( string ); ok{ fmt. Println( "got string" ) } else if uInt32, ok:= u.( int32 ); ok{ fmt. Println( "got int32" ) } else { panic( "not string|int32" ) } } func main() { DoThing( "wasd" ) // will panic! DoThing( 1 ) // will panic! var myVariable TheUnion= "qwerty" // ☝ This has a type of TheUnion, not string. // ✅ _= myVariable.( TheUnion) // ❌ _= myVariable.( string ) }
There’s not a good way to get the string
or int32
value out of TheUnion
in this example when using type U any
. If you can find one let me know! I’m sure there’s a way.
On a semantics note: the union type should not behave as a distinct type; it should act like the _union_ of all the terminal types. type U = any
is the best fit for that.
2.2.16. Buffer source types
Uint8Array
| []uint8 |
---|---|
Uint8ClampedArray
| []uint8 |
Int8Array
| []int8 |
Uint16Array
| []uint16 |
Int16Array
| []int16 |
Uint32Array
| []uint32 |
Int32Array
| []int32 |
BigUint64Array
| []uint64 |
BigInt64Array
| []int64 |
Float32Array
| []float32 |
Float64Array
| []float64 |
An ArrayBuffer
needs to support [AllowResizable]
so it’s represented in Go as a bytes.Buffer. SharedArrayBuffer
is a bit trickier. There’s no concurrent bytes.Buffer
in the Go standard library (that I know of) so for now it’s just bytes.Buffer
with the note that it should be used with caution.
partial interface Window {Float64Array doThing (ArrayBuffer b ); };
func DoThing( b* bytes. Buffer) [] float64 { return unsafe. Slice(( * float64 )( unsafe. Pointer( & b. Bytes()[ 0 ])), len( b. Bytes()) / unsafe. Sizeof( float64( 0 ))) }
A DataView
is roughly equivalent to a []byte
and can then be used with other encoding functions to use and mutate the slice’s contents.
2.2.17. Frozen arrays
Use a normal Go slice and just make sure you emphasize that it’s intended to be immutable.
2.2.18. Observable arrays
This is fairly rare. adoptedStyleSheets
is one case where I know this is used. Don’t know how to model this in Go.
2.3. Extended attributes
[AllowResizable] | Add doc comment to note that this bytes.Buffer instance can be resized. |
---|---|
[AllowShared] | Make sure to emphasize that this bytes.Buffer should be behind a concurrency lock. |
[Clamp] | Not relevant with exactly matching number types. |
[CrossOriginIsolated] | Assume that we are always cross-origin isolated. 🤷♀️ |
[Default] | I don’t know what this does. |
[EnforceRange] | Not relevant. Go has exactly matching number types. |
[Exposed] | Not relevant. every Web IDL definition is exposed to other Go code. |
[Global] | Any interface that has this should expose all its members in the package scope and not provide a concrete type itself. |
[NewObject] | Must always return a new instance such that f() != f() .
|
[Replaceable] | Not relevant. Go has a stricter type system. |
[SameObject] | Must always return the same instance such that f() == f() .
|
[SecureContext] | Assume that we are always in a secure context. 🤷♀️ |
[Unscopable] | Not relevant. Go has no with block.
|
-tags=coi
might be something that happens in the future to disable cross-origin isolated features by default.
2.3.1. [PutForwards]
This attribute was designed so that this would work:
[Exposed =Window ]interface Name {attribute DOMString full ;attribute DOMString family ;attribute DOMString given ; }; [Exposed =Window ]interface Person { [PutForwards =full ]readonly attribute Name name ;attribute unsigned short age ; };
var p= getPerson(); // Obtain an instance of Person. p. name= 'John Citizen' ; // This statement... p. name. full= 'John Citizen' ; // ...has the same behavior as this one.
Since this attribute can only be applied on readonly
attributes, the port to Go is simple: delegate the setter of the parent attribute to the specified child attribute. There’s no union type or any type deduction.
[Exposed =Window ]interface Name {attribute DOMString full ;attribute DOMString family ;attribute DOMString given ; }; [Exposed =Window ]interface Person { [PutForwards =full ]readonly attribute Name name ;attribute unsigned short age ; };
type Nameinterface { Full() string SetFull( vstring ) Family() string SetFamily( vstring ) Given() string SetGiven( vstring ) isName() } type Personinterface { Name() Name SetName( vstring ) Age() uint16 SetAge( vuint16 ) isPerson() } // ✅ The getter stays the same as always. This is a readonly property. func ( p* personImpl) Name() Name{ return p. name} // 👇 We delegate to the p.name.SetFull() just like [PutForwards=full] said! func ( p* personImpl) SetName( vstring ) { p. name. SetFull( v) }
This attribute is used in ElementCSSInlineStyle
/style
.
2.4. Legacy extended attributes
[LegacyFactoryFunction] | Define this function in the same way JavaScript would. |
---|---|
[LegacyLenientSetter] | Not relevant. |
[LegacyLenientThis] | Not relevant. |
[LegacyNamespace] | Interfaces will have the namespace name prefixed to their name. Used extensively in the WebAssembly APIs. |
[LegacyNoInterfaceObject] | Make the type private. Used in WebGL specifications. |
[LegacyNullToEmptyString] | Not implemented. |
[LegacyOverrideBuiltIns] | Not implemented. |
[LegacyTreatNonObjectAsNull] | Not implemented. |
[LegacyUnenumerableNamedProperties] | Not relevant. Go has no concept of iterable object properties. |
[LegacyUnforgeable] | Not relevant. All Go properties are unforgeable (not overridable) by default. |
[LegacyWindowAlias] | Do a type T = U type alias. Usually used for vendor-specific prefixes.
|
2.5. Overloads
Normally Web IDL overloads would be routed under a single JavaScript function. In Go the overload convention is different; you define overloads as separate functions with slightly different names. The naming convention for these extra functions usually focuses either on a type like Match
vs MatchString
or a named parameter like Split
vs SplitAfterN
.
The convention that this standard has adopted is to use TODO
2.6. Interfaces
TODO
2.6.1. Constants
TODO