This is no longer true. Keyword arguments are implemented using named tuples since Julia 0.7/1.0 and are now much much faster.
For example:
julia> f1(;x = 1, y = 2) = x + y
f1 (generic function with 1 method)
julia> f2(xy::NamedTuple) = xy.x + xy.y
f2 (generic function with 1 method)
julia> struct XY{T}
x::T
y::T
end
julia> f3(xy::XY) = xy.x + xy.y
f3 (generic function with 1 method)
julia> using BenchmarkTools
julia> @btime f1(x = 1, y = 2)
1.238 ns (0 allocations: 0 bytes)
3
julia> @btime f2((x=1, y=2))
0.015 ns (0 allocations: 0 bytes)
3
julia> @btime f3(XY(1, 2))
0.015 ns (0 allocations: 0 bytes)
All three cases are 1ns or faster. You may note that f2 and f3 appear to be much faster, but sub-1ns timings just indicate that the compiler has optimized out the entire function, since it can trivially prove that the result is unused. Setting that aside, all three options are sufficiently fast and you should pick whichever fits your use case best.
By the way, if you want to use a struct but don’t want to have to remember the argument order, I would recommend https://github.com/mauro3/Parameters.jl which makes it easy to define structs with more friendly constructors and default values:
And the author (mauro3) of Parameters stated that Parameter.jl will align with Base.@kwdef with some API breakage. Release 1.0? · Issue #121 · mauro3/Parameters.jl · GitHub.
There is another package of mauro3, Unpack.jl, doing @unpack a,b,c = p instead of a, b, c = p.a, p.b, p.c for dicts / structs / namedtuples / labelled arrays, which is handy IMHO.